From 2f09fd0ecb962824297ce001bdbc845c74b8173e Mon Sep 17 00:00:00 2001 From: onefang Date: Sun, 19 Apr 2020 14:18:53 +1000 Subject: Lots of changes, mostly a rewrite of how the dynamic pages work. --- src/sledjchisl/sledjchisl.c | 2855 +++++++++++++++++++++++++++---------------- 1 file changed, 1828 insertions(+), 1027 deletions(-) (limited to 'src/sledjchisl/sledjchisl.c') diff --git a/src/sledjchisl/sledjchisl.c b/src/sledjchisl/sledjchisl.c index 1c98575..2bc6719 100644 --- a/src/sledjchisl/sledjchisl.c +++ b/src/sledjchisl/sledjchisl.c @@ -308,7 +308,7 @@ qhashtbl_t *HTMLfileCache = NULL; typedef struct _reqData reqData; - +/* typedef int (*fieldValidFunc) (reqData *Rd, qhashtbl_t *data, char *name); typedef struct _validFunc validFunc; struct _validFunc @@ -316,6 +316,7 @@ struct _validFunc char *name, *title; fieldValidFunc func; }; + qlisttbl_t *fieldValidFuncs = NULL; static void newValidFunc(char *name, char *title, fieldValidFunc func) { @@ -324,6 +325,7 @@ static void newValidFunc(char *name, char *title, fieldValidFunc func) fieldValidFuncs->put(fieldValidFuncs, vf->name, vf, sizeof(validFunc)); free(vf); } +*/ typedef void *(*pageFunction) (char *file, reqData *Rd, HTMLfile *thisFile); typedef struct _dynPage dynPage; @@ -341,6 +343,7 @@ static void newDynPage(char *name, pageFunction func) free(dp); } +/* typedef void *(*pageBuildFunction) (reqData *Rd, char *message); typedef struct _buildPage buildPage; struct _buildPage @@ -356,42 +359,6 @@ static void newBuildPage(char *name, pageBuildFunction func, pageBuildFunction e buildPages->put(buildPages, bp->name, bp, sizeof(buildPage)); free(bp); } - - -/* TODO - there should be some precedence for values overriding values here. - Nothing official? - https://www.w3.org/standards/webarch/protocols - "This intro text is boilerplate for the beta release of w3.org." Fucking useless. Pffft - https://www.w3.org/Protocols/ - Still nothing official, though the ENV / HEADER stuff tends to be about the protocol things, and cookies / body / queries are about the data things. - -TODO - I think this is the wrong question, mostly data from different sources is for different reasons. - -Also including values from the database. - -URL query Values actually provided by the user in the FORM, and other things. -POST body Values actually provided by the user in the FORM. -cookies - https://stackoverflow.com/questions/4056306/how-to-handle-multiple-cookies-with-the-same-name - -headers includes HTTP_COOKIE and QUERY_STRING -env includes headers and HTTP_COOKIE and QUERY_STRING - -database Since all of the above are for updating the database anyway, this goes on the bottom, overridden by all. - Though be wary of security stuff. - -We don't actually get the headers directly, it's all sent via the env. - -http://docs.gantry.org/gantry4/advanced/setby - Says that query overrides cookies, but that might be just for their platform. - -https://framework.zend.com/manual/1.11/en/zend.controller.request.html - Says - "1. GET, 2. POST, 3. COOKIE, 4. SERVER, 5. ENV." - - -Sending cookie headers is a special case, multiples can be sent, otherwise headers are singletons, only send one for each name. - -local storage? Would be client side Javascript thing not usually sent back to server. */ #define HMACSIZE EVP_MAX_MD_SIZE * 2 @@ -401,8 +368,9 @@ struct _sesh { char salt[256 + 1], seshID[256 + 1], sesh[256 + 16 + 10 + 1], munchie[HMACSIZE + 16 + 10 + 1], toke_n_munchie[HMACSIZE + 1], hashish[HMACSIZE + 1], - leaf[HMACSIZE64 + 6 + 1]; + leaf[HMACSIZE64 + 6 + 1], *UUID, *name; struct timespec timeStamp[2]; + short level; boolean isLinky; }; @@ -410,13 +378,13 @@ struct _reqData { lua_State *L; qhashtbl_t *configs, *queries, *body, *cookies, *headers, *valid, *stuff, *database, *Rcookies, *Rheaders; - char *Scheme, *Host, *Method, *Script, *RUri, *doit; + char *Scheme, *Host, *Method, *Script, *RUri, *doit, *form, *output; sesh shs, *lnk; MYSQL *db; gridStats *stats; qlist_t *errors, *messages; qgrow_t *reply; - pageBuildFunction func; +// pageBuildFunction func; struct timespec then; boolean chillOut, vegOut; }; @@ -428,14 +396,19 @@ static void showSesh(qgrow_t *reply, sesh *shs) 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);
+  if (NULL != shs->name)
+    reply->addstrf(reply, "   name = %s\n", shs->name);
+  if (NULL != shs->UUID)
+    reply->addstrf(reply, "   UUID = %s\n", shs->UUID);
+  reply->addstrf(reply,   "   salt = %s\n", shs->salt);
+  reply->addstrf(reply,   "   seshID = %s\n", shs->seshID);
+  reply->addstrf(reply,   "   timeStamp = %ld.%ld\n", shs->timeStamp[1].tv_sec, shs->timeStamp[1].tv_nsec);
+  reply->addstrf(reply,   "   sesh = %s\n", shs->sesh);
+  reply->addstrf(reply,   "   munchie = %s\n", shs->munchie);
+  reply->addstrf(reply,   "   toke_n_munchie = %s\n", shs->toke_n_munchie);
+  reply->addstrf(reply,   "   hashish = %s\n", shs->hashish);
+  reply->addstrf(reply,   "   leaf = %s\n", shs->leaf);
+  reply->addstrf(reply,   "   level = %d\n", (int) shs->level);
   reply->addstr(reply, "
\n"); } @@ -843,7 +816,7 @@ static void PrintEnv(qgrow_t *reply, char *label, char **envp) static void printEnv(char **envp) { for ( ; *envp != NULL; envp++) - d("%s", *envp); + D("%s", *envp); } @@ -905,6 +878,49 @@ I suspect most will be of the form - UPDATE items,month SET items.price=month.price WHERE items.id=month.id; */ +static boolean dbConnect() +{ + dbconn = mysql_real_connect(database, + getStrH(configs, "Data Source"), + getStrH(configs, "User ID"), + getStrH(configs, "Password"), + getStrH(configs, "Database"), +// 3036, "/var/run/mysqld/mysqld.sock", + 0, NULL, + CLIENT_FOUND_ROWS | CLIENT_LOCAL_FILES | CLIENT_MULTI_STATEMENTS | CLIENT_MULTI_RESULTS); + if (NULL == dbconn) + { + E("mysql_real_connect() failed - %s", mysql_error(database)); + return FALSE; + } + return TRUE; +} + +// A general error function that checks for certain errors that mean we should try to connect to the server MariaDB again. +// https://mariadb.com/kb/en/mariadb-error-codes/ +// 1129? 1152? 1184? 1218? 1927 3032? 4150? +// "server has gone away" isn't listed there, that's the one I was getting. Pffft +// It's 2006, https://dev.mysql.com/doc/refman/8.0/en/gone-away.html +// Though none of the mentioned reasons make sense here. +// Ah it could be "connection inactive for 8 hours". +// Which might be why OpenSim opens a new connection for EVERYTHING. +// TODO - see if I can either find out what the time out is, or just check and re open for each db thing. +// int mysql_ping(MYSQL * mysql); // https://mariadb.com/kb/en/mysql_ping/ +// "If it has gone down, and global option reconnect is enabled an automatic reconnection is attempted." +// "Returns zero on success, nonzero if an error occured." +// "resources bundled to the connection (prepared statements, locks, temporary tables, ...) will be released." sigh +// Quick'n'dirty until this is properly event driven - have a cron job curl the stats page every hour. +static boolean dbCheckError(MYSQL *db, char *error, char *sql) +{ + int e = mysql_errno(db); + + E("MariaDB error %d - %s: %s\n%s", e, error, mysql_error(db), sql); + if (2006 == e) + return dbConnect(); + + return FALSE; +} + typedef struct _dbField dbField; struct _dbField { @@ -929,19 +945,27 @@ qlisttbl_t *dbGetFields(MYSQL *db, char *table) d("Getting field metadata for %s", table); if (mysql_query(db, sql)) - E("Query failed: %s\n%s", mysql_error(db), sql); + { +// E("MariaDB error %d - Query failed 0: %s\n%s", mysql_errno(db), mysql_error(db), sql); + if (dbCheckError(db, "Query failed 0", sql)) + { + ret = dbGetFields(db, table); + free(sql); + return ret; + } + } else { MYSQL_RES *res = mysql_store_result(db); if (!res) - E("Couldn't get results set from %s\n %s", mysql_error(db), sql); + E("MariaDB error %d - Couldn't get results set from %s\n %s", mysql_errno(db), mysql_error(db), sql); else { MYSQL_FIELD *fields = mysql_fetch_fields(res); if (!fields) - E("Failed fetching fields: %s", mysql_error(db)); + E("MariaDB error %d - Failed fetching fields: %s", mysql_errno(db), mysql_error(db)); else { unsigned int i, num_fields = mysql_num_fields(res); @@ -1080,6 +1104,7 @@ d("New SQL statement - %s", req->sql); goto freeIt; } req->inBind = xzalloc(i * sizeof(MYSQL_BIND)); +W("Allocated %d %d inBinds for %s", i, req->inCount, req->sql); for (i = 0; i < req->inCount; i++) { dbField *fld = req->flds->get(req->flds, req->inParams[i], NULL, false); @@ -1185,7 +1210,7 @@ d("New SQL statement - %s", req->sql); 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)); + D(" mysql_stmt_result_metadata() error %d, returned no meta information - %s\n", mysql_stmt_errno(req->prep), mysql_stmt_error(req->prep)); goto freeIt; } @@ -1221,6 +1246,7 @@ I("count!!!!!!!!!!!!!!!!"); goto freeIt; } req->outBind = xzalloc(i * sizeof(MYSQL_BIND)); +W("Allocated %d %d outBinds for %s", i, req->outCount, req->sql); for (i = 0; i < req->outCount; i++) { dbField *fld = req->flds->get(req->flds, req->outParams[i], NULL, false); @@ -1335,7 +1361,7 @@ I("count!!!!!!!!!!!!!!!!"); } if (mysql_stmt_bind_result(req->prep, req->outBind)) { - E("Bind failed."); + E("Bind failed error %d.", mysql_stmt_errno(req->prep)); goto freeIt; } } @@ -1467,7 +1493,7 @@ I("count!!!!!!!!!!!!!!!!"); } if (mysql_stmt_bind_param(req->prep, req->inBind)) { - E("Bind failed."); + E("Bind failed error %d.", mysql_stmt_errno(req->prep)); goto freeIt; } @@ -1477,7 +1503,7 @@ 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)); + E("Statement execute failed %d: %s\n", mysql_stmt_errno(req->prep), mysql_stmt_error(req->prep)); goto freeIt; } @@ -1489,7 +1515,7 @@ d("Execute %s", req->sql); 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)); + E(" mysql_stmt_store_result() failed %d: %s", mysql_stmt_errno(req->prep), mysql_stmt_error(req->prep)); goto freeIt; } req->rowCount = mysql_stmt_num_rows(req->prep); @@ -1618,7 +1644,7 @@ 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)); + E("Statement result freeing failed %d: %s\n", mysql_stmt_errno(req->prep), mysql_stmt_error(req->prep)); end: va_end(ap); @@ -1643,6 +1669,7 @@ void dbPull(reqData *Rd, char *table, rowData *rows) while(me->getnext(me, &obj, false) == true) { where = xmprintf("%s.%s", table, obj.name); +d("dbPull(Rd->database) %s = %s", where, (char *) obj.data); Rd->database->putstr(Rd->database, where, (char *) obj.data); // me->remove(me, obj.name); free(where); @@ -1655,11 +1682,13 @@ void dbFreeRequest(dbRequest *req) { int i; - D("Cleaning up prepared database request %s - %s", req->table, req->where); + D("Cleaning up prepared database request %s - %s %d %d", req->table, req->where, req->outCount, req->inCount); if (NULL != req->outBind) { +d("Free outBind"); for (i = 0; i < req->outCount; i++) { +d("Free outBind %d %s", i, req->sql); 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); @@ -1669,8 +1698,10 @@ void dbFreeRequest(dbRequest *req) } if (NULL != req->inBind) { +d("Free inBind"); for (i = 0; i < req->inCount; i++) { +d("Free inBind %d %s", i, req->sql); // TODO - this leaks for some bizare reason. if (NULL != req->inBind[i].buffer) free(req->inBind[i].buffer); if (NULL != req->inBind[i].length) free(req->inBind[i].length); @@ -1705,7 +1736,15 @@ my_ulonglong dbCount(MYSQL *db, char *table, char *where) sql = xmprintf("SELECT Count(*) FROM %s", table); if (mysql_query(db, sql)) - E("Query failed: %s", mysql_error(db)); + { +// E("MariaDB error %d - Query failed 1: %s", mysql_errno(db), mysql_error(db)); + if (dbCheckError(db, "Query failed 1", sql)) + { + ret = dbCount(db, table, where); + free(sql); + return ret; + } + } else { MYSQL_RES *result = mysql_store_result(db); @@ -1716,7 +1755,7 @@ my_ulonglong dbCount(MYSQL *db, char *table, char *where) { MYSQL_ROW row = mysql_fetch_row(result); if (!row) - E("Couldn't get row from %s\n: %s", sql, mysql_error(db)); + E("MariaDB error %d - Couldn't get row from %s\n: %s", mysql_errno(db), sql, mysql_error(db)); else ret = atoll(row[0]); mysql_free_result(result); @@ -1752,13 +1791,21 @@ my_ulonglong dbCountJoin(MYSQL *db, char *table, char *select, char *join, char sql = xmprintf("SELECT %s FROM %s", select, table, join); if (mysql_query(db, sql)) - E("Query failed: %s", mysql_error(db)); + { +// E("MariaDB error %d - Query failed 2: %s", mysql_errno(db), mysql_error(db)); + if (dbCheckError(db, "Query failed 2", sql)) + { + ret = dbCountJoin(db, table, select, join, where); + free(sql); + return ret; + } + } else { MYSQL_RES *result = mysql_store_result(db); if (!result) - E("Couldn't get results set from %s\n: %s", sql, mysql_error(db)); + E("MariaDB error %d - Couldn't get results set from %s\n: %s", mysql_errno(db), sql, mysql_error(db)); else ret = mysql_num_rows(result); mysql_free_result(result); @@ -1801,12 +1848,12 @@ MYSQL_RES *dbSelect(MYSQL *db, char *table, char *select, char *join, char *wher } if (mysql_query(db, sql)) - E("Query failed: %s\n%s", mysql_error(db), sql); + E("MariaDB error %d - Query failed 3: %s\n%s", mysql_errno(db), 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); + E("MariaDB error %d - Couldn't get results set from %s\n %s", mysql_errno(db), mysql_error(db), sql); } if (-1 == clock_gettime(CLOCK_REALTIME, &now)) @@ -2040,6 +2087,14 @@ void santize(qhashtbl_t *tbl, bool decode) tbl->unlock(tbl); } +/* +char *unsantize(char *str) +{ + char *ret = qurl_decode(xstrdup(str)); + return ret; +} +*/ + void outize(qgrow_t *reply, qhashtbl_t *tbl, char *label) { reply->addstrf(reply, "%s:
\n
\n", label);
@@ -2052,6 +2107,42 @@ void outize(qgrow_t *reply, qhashtbl_t *tbl, char *label)
   reply->addstr(reply, "
\n"); } +char *displayPrep(char *str) +{ + char *ret = xstrdup(str), *t; + + qurl_decode(ret); + t = qstrreplace("tn", ret, "<", "<"); + free(ret); + ret = NULL; + if (NULL != t) + { + ret = qstrreplace("tn", t, ">", ">"); + if (NULL == ret) + ret = t; + else + free(t); + } + + if (NULL == ret) + ret = xstrdup(str); + + return ret; +} + +char *encodeSlash(char *str) +{ + char *ret = xstrdup(str), *t = qstrreplace("tn", str, "\\", "%5c"); + + if (NULL != t) + { + free(ret); + ret = t; + } + + return ret; +} + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie enum cookieSame @@ -2143,6 +2234,21 @@ struct _fragment char *text; }; +static void HTMLdebug(qgrow_t *reply) +{ + reply->addstrf(reply, + "

\n" + "

\n" + "

DEBUG

\n" + "
\n" + "

DEBUG log

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

\n" + ); +} + static void HTMLheader(qgrow_t *reply, char *title) { reply->addstrf(reply, @@ -2152,6 +2258,7 @@ static void HTMLheader(qgrow_t *reply, char *title) " \n" " \n" , title); + reply->addstrf(reply, " \n"); if (DEBUG) reply->addstrf(reply, " \n"); @@ -2172,25 +2279,11 @@ static void HTMLheader(qgrow_t *reply, char *title) " \n" " \n" " \n" - " " + " \n" ); -} - -static void HTMLdebug(qgrow_t *reply) -{ - reply->addstrf(reply, - "
\n" - "

\n" - "

\n" - "

DEBUG

\n" - "
\n" - "

DEBUG log

\n" - " " - "
\n" - "
\n" - "

\n" - "
\n" - ); + reply->addstrf(reply, "
\n"); + if (DEBUG) + HTMLdebug(reply); } static void HTMLtable(qgrow_t *reply, MYSQL *db, MYSQL_RES *result, char *caption, char *URL, char *id) @@ -2259,7 +2352,8 @@ static void HTMLtable(qgrow_t *reply, MYSQL *db, MYSQL_RES *result, char *captio static void HTMLhidden(qgrow_t *reply, char *name, char *val) { - reply->addstrf(reply, " \n", name, val); + if ((NULL != val) && ("" != val)) + reply->addstrf(reply, " \n", name, val); } static void HTMLform(qgrow_t *reply, char *action, char *token) @@ -2270,7 +2364,7 @@ static void HTMLform(qgrow_t *reply, char *action, char *token) } static void HTMLformEnd(qgrow_t *reply) { - reply->addstr(reply, " \n"); + reply->addstrf(reply, " \n"); } static void HTMLcheckBox(qgrow_t *reply, char *name, char *title, boolean checked, boolean required) @@ -2288,7 +2382,7 @@ static void HTMLcheckBox(qgrow_t *reply, char *name, char *title, boolean checke reply->addstrf(reply, "

\n"); } -static void HTMLtextArea(qgrow_t *reply, char *name, char *title, int rows, int cols, int min, int max, char *holder, char *comp, char *spell, char *wrap, char *value, boolean required) +static void HTMLtextArea(qgrow_t *reply, char *name, char *title, int rows, int cols, int min, int max, char *holder, char *comp, char *spell, char *wrap, char *val, boolean required, boolean readOnly) { reply->addstrf(reply, "

\n", value); + if ((NULL != val) && ("" != val)) + reply->addstrf(reply, ">%s

\n", val); + else + reply->addstrf(reply, ">

\n"); } static void HTMLtext(qgrow_t *reply, char *type, char *title, char *name, char *val, int size, int max, boolean required) { reply->addstrf(reply, "

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

\n"); + reply->addstr(reply, "

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

"); } static void HTMLoption(qgrow_t *reply, char *title, boolean selected) @@ -2426,9 +2525,28 @@ void HTMLfill(reqData *Rd, enum fragmentType type, char *text, int length) static void HTMLfooter(qgrow_t *reply) { - reply->addstr(reply, - "
" - " \n\n"); + reply->addstrf(reply, "
\n"); + reply->addstr(reply, + "
\n" + "

Test mode

\n" + "

This account manager system is currently in test mode, and under heavy development.   " + " Which means it not all written yet, and things may break.

\n" + "

Your mission, should you choose to accept it, is to break it, and report how you broke it, so that onefang can fix it.

\n" + "

During test mode, no real grid accounts are created, and any accounts created with this will be deleted later.   " + " So feel free to create as many test accounts as you need to test things.

\n" + "

We follow the usual web site registration process, which sends a validation email, with a link to click.   " + " However, during this test mode, no emails will be sent, instead a link will be displayed near the top of the page when a user is logged in.

\n" + "

Missing bits that are still being written - sending the emails, creating real grid accounts, editing accounts, listing accounts, deleting accounts.

\n" + "
\n"); +// reply->addstr(reply, "
\n
\n"); + reply->addstr(reply, +// "
\n" +// "
\n" + "
\n" + " \n" + "
\n" + "
\n" + "\n\n"); } @@ -2596,6 +2714,9 @@ HTMLfile *checkHTMLcache(char *file) } + + + /* TODO - On new user / password reset. @@ -2713,7 +2834,7 @@ https://stackoverflow.com/questions/549/the-definitive-guide-to-form-based-websi // Forward declare this here so we can use it in validation functions. -void loginPage(reqData *Rd, char *message); +//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 @@ -2790,10 +2911,13 @@ https://stackoverflow.com/questions/16891729/best-practices-salting-peppering-pa */ +qlisttbl_t *accountLevels = NULL; + + static void bitch(reqData *Rd, char *message, char *log) { addStrL(Rd->errors, message); - E("%s %s %s %s %s", getStrH(Rd->headers, "REMOTE_ADDR"), getStrH(Rd->stuff, "UUID"), getStrH(Rd->stuff, "name"), message, log); + E("%s %s %s - %s %s", getStrH(Rd->headers, "REMOTE_ADDR"), Rd->shs.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." @@ -2806,11 +2930,79 @@ I think this means send a new cookie. 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); + C("%s %s %s - %s %s", getStrH(Rd->headers, "REMOTE_ADDR"), Rd->shs.UUID, getStrH(Rd->stuff, "name"), message, log); Rd->vegOut = TRUE; } +// The ancient, insecure since 2011, Second Life / OpenSim password hashing algorithm. +char *newSLOSsalt(reqData *Rd) +{ + char *salt = NULL; + unsigned char *md5hash = xzalloc(17); + char uuid[37]; + uuid_t binuuid; + + uuid_generate_random(binuuid); + uuid_unparse_lower(binuuid, uuid); + if (!qhashmd5((void *) uuid, strlen(uuid), md5hash)) + bitch(Rd, "Internal error.", "newSLOSsalt() - qhashmd5(new uuid) failed."); + else + salt = qhex_encode(md5hash, 16); + free(md5hash); + return salt; +} + +/* TODO - rewrite this - + Don't store things in Rd->stuff. Salt was passed in, and not modified. + Return calculated passHash, not int ret. Returns a NULL if things went wrong. +*/ +char *checkSLOSpassword(reqData *Rd, char *salt, char *password, char *passwordHash, char *fail) +{ + char *ret = NULL; + int rt = 0; + unsigned char *md5hash = xzalloc(17); + char *hash = NULL, *passHash = NULL; + +T("checkSLOSpassword(%s, %s, %s, ", password, salt, passwordHash, fail); + // Calculate passHash. + if (!qhashmd5((void *) password, strlen(password), md5hash)) + { + bitch(Rd, "Internal error.", "checkSLOSpassword() - qhashmd5(password) failed."); + rt++; + } + else + { + passHash = qhex_encode(md5hash, 16); + hash = xmprintf("%s:%s", passHash, salt); + if (!qhashmd5((void *) hash, strlen(hash), md5hash)) + { + bitch(Rd, "Internal error.", "checkSLOSpassword() - qhashmd5(password:salt) failed."); + rt++; + } + else + { + ret = qhex_encode(md5hash, 16); + } + free(hash); + free(passHash); + } + + // If one was passed in, compare it. + if ((NULL != ret) && (NULL != passwordHash) && (strcmp(ret, passwordHash) != 0)) + { + bitch(Rd, fail, "Password doesn't match passwordHash"); + E(" %s %s - %s != %s", password, salt, ret, passwordHash); + rt++; + free(ret); + ret = NULL; + } + free(md5hash); + + return ret; +} + + int LuaToHash(reqData *Rd, char *file, char *var, qhashtbl_t *tnm, int ret, struct stat *st, struct timespec *now, char *type) { struct timespec then; @@ -2856,7 +3048,7 @@ int LuaToHash(reqData *Rd, char *file, char *var, qhashtbl_t *tnm, int ret, stru if (lua_isstring(Rd->L, -1)) { tnm->putstr(tnm, n, (char *) lua_tostring(Rd->L, -1)); -//d("Reading %s = %s", n, getStrH(tnm, n)); +d("Lua reading (%s) %s = %s", type, n, getStrH(tnm, n)); } else { @@ -2879,11 +3071,28 @@ int LuaToHash(reqData *Rd, char *file, char *var, qhashtbl_t *tnm, int ret, stru } +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; +} + + static void freeSesh(reqData *Rd, boolean linky, boolean wipe) { char *file = NULL; sesh *shs = &Rd->shs; +T("free sesh %s %s", linky ? "linky" : "session", wipe ? "wipe" : "delete"); if (linky) { shs = Rd->lnk; @@ -2914,19 +3123,27 @@ static void freeSesh(reqData *Rd, boolean linky, boolean wipe) 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"); + 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); + shs->name = NULL; + shs->UUID = NULL; + shs->level = -256; +// TODO - should I wipe the rest of Rd->shs as well? Rd->stuff->remove(Rd->stuff, "name"); - Rd->stuff->remove(Rd->stuff, "level"); + Rd->stuff->remove(Rd->stuff, "firstName"); + Rd->stuff->remove(Rd->stuff, "lastName"); + Rd->stuff->remove(Rd->stuff, "email"); Rd->stuff->remove(Rd->stuff, "passwordSalt"); Rd->stuff->remove(Rd->stuff, "passwordHash"); + Rd->stuff->remove(Rd->stuff, "passHash"); + Rd->stuff->remove(Rd->stuff, "passSalt"); + Rd->stuff->remove(Rd->stuff, "linky-hashish"); } if (shs->isLinky) @@ -2935,14 +3152,16 @@ static void freeSesh(reqData *Rd, boolean linky, boolean wipe) Rd->lnk = NULL; } else + { shs->leaf[0] = '\0'; + } free(file); } static void setToken_n_munchie(reqData *Rd, boolean linky) { sesh *shs = &Rd->shs; - char *file, *link = ""; + char *file; if (linky) { @@ -2952,8 +3171,6 @@ static void setToken_n_munchie(reqData *Rd, boolean linky) else { file = xmprintf("%s/sessions/%s.lua", scCache, shs->leaf); - if (NULL != Rd->lnk) - link = Rd->lnk->hashish; } struct stat st; @@ -2968,45 +3185,77 @@ static void setToken_n_munchie(reqData *Rd, boolean linky) "{\n" " ['IP']='%s',\n" " ['salt']='%s',\n" - " ['seshID']='%s',\n" - " ['linky-hashishy']='%s',\n", + " ['seshID']='%s',\n", getStrH(Rd->headers, "REMOTE_ADDR"), shs->salt, - shs->seshID, - link + shs->seshID ); - char *tnm1 = xmprintf("}\n" + char *tnm1 = xmprintf(" ['name']='%s',\n", shs->name); + char *tnm2 = xmprintf(" ['UUID']='%s',\n", shs->UUID); + char *tnm3 = xmprintf(" ['passHash']='%s',\n", getStrH(Rd->stuff, "passHash")); + char *tnm4 = xmprintf(" ['passSalt']='%s',\n", getStrH(Rd->stuff, "passSalt")); + char *tnm9 = xmprintf("}\n" "return toke_n_munchie\n"); int fd = notstdio(xcreate_stdio(file, O_CREAT | O_WRONLY | O_TRUNC | O_CLOEXEC, S_IRUSR | S_IWUSR)); size_t l = strlen(tnm0); + if (s) I("Creating session %s.", file); else C("Updating session %s.", file); // I don't think updates can occur now. +t("Write shs %s", tnm0); if (l != writeall(fd, tnm0, l)) { perror_msg("Writing %s", file); freeSesh(Rd, linky, TRUE); } - qhashtbl_obj_t obj; + if (NULL != shs->name) + { +t("Write shs %s", tnm1); + l = strlen(tnm1); + if (l != writeall(fd, tnm1, l)) + { + perror_msg("Writing %s", file); + freeSesh(Rd, linky, TRUE); + } + } + if (NULL != shs->UUID) + { +t("Write shs %s", tnm2); + l = strlen(tnm2); + if (l != writeall(fd, tnm2, l)) + { + perror_msg("Writing %s", file); + freeSesh(Rd, linky, TRUE); + } + } - memset((void*)&obj, 0, sizeof(obj)); - Rd->stuff->lock(Rd->stuff); - while(Rd->stuff->getnext(Rd->stuff, &obj, false) == true) + if ('\0' != getStrH(Rd->stuff, "passHash")[0]) + { +t("Write shs %s", tnm3); + l = strlen(tnm3); + if (l != writeall(fd, tnm3, l)) + { + perror_msg("Writing %s", file); + freeSesh(Rd, linky, TRUE); + } + } + + if ('\0' != getStrH(Rd->stuff, "passSalt")[0]) { -t("stuff %s = %s", obj.name, (char *) obj.data); - if (dprintf(fd, " ['%s'] = '%s',\n", obj.name, (char *) obj.data) < 0) +t("Write shs %s", tnm4); + l = strlen(tnm4); + if (l != writeall(fd, tnm4, l)) { perror_msg("Writing %s", file); freeSesh(Rd, linky, TRUE); } } - Rd->stuff->unlock(Rd->stuff); - l = strlen(tnm1); - if (l != writeall(fd, tnm1, l)) + l = strlen(tnm9); + if (l != writeall(fd, tnm9, l)) { perror_msg("Writing %s", file); freeSesh(Rd, linky, TRUE); @@ -3014,14 +3263,71 @@ t("stuff %s = %s", obj.name, (char *) obj.data); // Set the mtime on the file. futimens(fd, shs->timeStamp); xclose(fd); + free(tnm9); + free(tnm2); free(tnm1); free(tnm0); free(file); } -static void createUser(reqData *Rd) + +static void generateAccountUUID(reqData *Rd) +{ + // Generate a UUID, check it isn't already being used. + char uuid[37], *where; + uuid_t binuuid; + my_ulonglong users = 0; + int c; + + do // UserAccounts.PrincipalID is a unique primary index anyway, but we want the user creation process to be a little on the slow side. + { + struct stat st; + + uuid_generate_random(binuuid); + uuid_unparse_lower(binuuid, uuid); + // Try Lua user file. + where = xmprintf("%s/users/%s.lua", scData, uuid); + c = stat(where, &st); + if (c) + users = 1; + free(where); + // Try database. + where = xmprintf("UserAccounts.PrincipalID = '%s'", uuid); + D("Trying new UUID %s.", where); + users = dbCount(Rd->db, "UserAccounts", where); + free(where); + } while (users != 0); + Rd->shs.UUID = xstrdup(uuid); + Rd->shs.level = -200; + Rd->database->putstr(Rd->database, "UserAccounts.PrincipalID", uuid); + Rd->database->putstr(Rd->database, "UserAccounts.Userlevel", "-200"); +} + +char *getLevel(reqData *Rd) +{ + char *ret = "", *lvl = xmprintf("%d", Rd->shs.level); + ret = accountLevels->getstr(accountLevels, lvl, false); + if (NULL == ret) + { + qlisttbl_obj_t obj; + + memset((void*)&obj, 0, sizeof(obj)); // must be cleared before call + accountLevels->lock(accountLevels); + while(accountLevels->getnext(accountLevels, &obj, NULL, false) == true) + { + if (atoi(obj.name) <= Rd->shs.level) + ret = (char *) obj.data; + } + } + free(lvl); + return ret; +} + +static void accountWrite(reqData *Rd) { - char *file = xmprintf("%s/users/%s.lua", scData, getStrH(Rd->stuff, "UUID")); + char *file = xmprintf("%s/users/%s.lua", scData, Rd->shs.UUID); + char *link = (NULL == Rd->lnk) ? "" : Rd->lnk->hashish; + char *about = encodeSlash(getStrH(Rd->stuff, "aboutMe")); char *tnm = xmprintf( "user = \n" "{\n" " ['name']='%s',\n" @@ -3032,32 +3338,33 @@ static void createUser(reqData *Rd) " ['level']='%d',\n" " ['flags']='%d',\n" " ['active']='%d',\n" - " ['passwordSalt']='%s',\n" " ['passwordHash']='%s',\n" + " ['passwordSalt']='%s',\n" " ['UUID']='%s',\n" - " ['DoB']='%s-%s',\n" + " ['DoB']='%s',\n" " ['agree']='%s',\n" " ['adult']='%s',\n" " ['aboutMe']='%s',\n" " ['vouched']='%s',\n" + " ['linky-hashish']='%s',\n" "}\n" "return user\n", getStrH(Rd->stuff, "name"), - (long) Rd->shs.timeStamp[1].tv_sec, - getStrH(Rd->body, "email"), - "newbie", - -200, + (strcmp("", getStrH(Rd->stuff, "created")) != 0) ? atol(getStrH(Rd->stuff, "created")) : (long) Rd->shs.timeStamp[1].tv_sec, + getStrH(Rd->stuff, "email"), + getLevel(Rd), + Rd->shs.level, 64, 0, - getStrH(Rd->stuff, "passwordSalt"), getStrH(Rd->stuff, "passwordHash"), - getStrH(Rd->stuff, "UUID"), - getStrH(Rd->body, "year"), - getStrH(Rd->body, "month"), - getStrH(Rd->body, "agree"), - getStrH(Rd->body, "adult"), - getStrH(Rd->body, "aboutMe"), - "off" + getStrH(Rd->stuff, "passwordSalt"), + Rd->shs.UUID, + getStrH(Rd->stuff, "DoB"), + getStrH(Rd->stuff, "agree"), + getStrH(Rd->stuff, "adult"), + about, + "off", + link ); struct stat st; @@ -3078,13 +3385,15 @@ static void createUser(reqData *Rd) char *nm = xmprintf("%s/users/%s.lua", scData, qstrreplace("tr", name, " ", "_")); free(file); - file = xmprintf("%s.lua", getStrH(Rd->stuff, "UUID")); + file = xmprintf("%s.lua", Rd->shs.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(tnm); + free(about); free(file); } @@ -3096,11 +3405,12 @@ static sesh *newSesh(reqData *Rd, boolean linky) uuid_t binuuid; sesh *ret = &Rd->shs; -d("New sesh"); +T("new sesh %s %s %s", linky ? "linky" : "session", ret->UUID, ret->name); if (linky) { Rd->lnk = xzalloc(sizeof(sesh)); ret = Rd->lnk; + ret->UUID = Rd->shs.UUID; } char buf[128]; // 512 bits. @@ -3145,7 +3455,15 @@ d("New sesh"); free(t1); qstrcpy(ret->munchie, sizeof(ret->munchie), munchie); //d("munchie %s", ret->munchie); - t0 = xmprintf("%s%s", getStrH(Rd->stuff, "UUID"), munchie); +// TODO - chicken and egg? Used to be from stuff->UUID. + t1 = ret->UUID; + if (NULL == t1) + { + uuid_clear(binuuid); + uuid_unparse_lower(binuuid, uuid); + ret->UUID = uuid; + } + t0 = xmprintf("%s%s", ret->UUID, munchie); free(munchie); toke_n_munchie = myHMAC(t0, FALSE); free(t0); @@ -3154,11 +3472,11 @@ d("New sesh"); hashish = myHMACkey(ret->salt, toke_n_munchie, FALSE); free(toke_n_munchie); qstrcpy(ret->hashish, sizeof(ret->hashish), hashish); -//d("hashish %s", ret->hashish); +d("hashish %s", ret->hashish); t0 = myHMACkey(getStrH(Rd->configs, "pepper"), hashish, TRUE); free(hashish); qstrcpy(ret->leaf, sizeof(ret->leaf), t0); -//d("leaf %s", ret->leaf); +d("leaf %s", ret->leaf); free(t0); ret->isLinky = linky; setToken_n_munchie(Rd, linky); @@ -3170,92 +3488,84 @@ d("New sesh"); return ret; } -char *checkLinky(reqData *Rd) -{ - char *ret = xstrdup(""), *t0 = getStrH(Rd->stuff, "linky-hashish"); - if ('\0' != t0[0]) - { - char *t1 = qurl_encode(t0, strlen(t0)); - free(ret); - ret = xmprintf("

You have an email waiting with a linky in it %s.

\n", - Rd->Host, Rd->RUri, t1, t0); - free(t1); - } - return ret; -} -boolean badBoy(int ret, reqData *Rd, qhashtbl_t *data, char *name, char *value) -{ - if (NULL == value) - value = getStrH(data, name); - if (0 != ret) - { - Rd->valid->putstr(Rd->valid, name, "-1"); -W("Bad boy %s = %s", name, value); - return TRUE; - } - Rd->stuff->putstr(Rd->stuff, name, value); - Rd->valid->putstr(Rd->valid, name, "1"); - return FALSE; -} +/* CRUD (Create, Read, Update, Delete) +CRAP (Create, Replicate, Append, Process) +Though I prefer - +DAVE (Delete, Add, View, Edit), coz the names are shorter. B-) +On the other hand, list or browse needs to be added, which is why they have +BREAD (Browse, Read, Edit, Add, Delete) +CRUDL (Create, Read, Update, Delete, List) +CRUDE (Create, Read, Update, Delete, Experience) +Maybe - +DAVEE (Delete, Add, View, Edit, Explore) +*/ -int validateThings(reqData *Rd, char *doit, char *name, qhashtbl_t *things) +// lua.h has LUA_T* NONE, NIL, BOOLEAN, LIGHTUSERDATA, NUMBER, STRING, TABLE, FUNCTION, USERDATA, THREAD as defines, -1 - 8. +// These are the missing ones. Then later we will have prim, mesh, script, sound, terrain, ... +#define LUA_TGROUP 42 +#define LUA_TINTEGER 43 +#define LUA_TEMAIL 44 +#define LUA_TPASSWORD 45 +#define LUA_TFILE 46 +#define LUA_TIMAGE 47 + +#define FLD_NONE 0 +#define FLD_EDITABLE 1 +#define FLD_HIDDEN 2 +#define FLD_REQUIRED 4 + +typedef struct _inputField inputField; +typedef struct _inputSub inputSub; +typedef struct _inputForm inputForm; +typedef struct _inputValue inputValue; + +typedef int (*inputFieldValidFunc) (reqData *Rd, inputForm *iF, inputValue *iV); +typedef void (*inputFieldShowFunc) (reqData *Rd, inputForm *iF, inputValue *iV); +typedef int (*inputSubmitFunc) (reqData *Rd, inputForm *iF, inputValue *iV); +typedef void (*inputFormShowFunc) (reqData *Rd, inputForm *iF, inputValue *iV); + +struct _inputField { - int e = 0; - qlisttbl_obj_t obj; + char *name, *title, *help; + inputFieldValidFunc validate; // Alas C doesn't have any anonymous function standard. + inputFieldShowFunc web, console, gui; + inputField **group; // If this is a LUA_TGROUP, then this will be a null terminated array of the fields in the group. +// database details +// lua file details + signed char type, flags; + short editLevel, viewLevel, viewLength, maxLength; +}; +struct _inputSub +{ + char *name, *title, *help, *outputForm; + inputSubmitFunc submit; +}; +struct _inputForm +{ + char *name, *title, *help; + qlisttbl_t *fields; // qlisttbl coz iteration in order and lookup are important. + qhashtbl_t *subs; + inputFormShowFunc web, eWeb; // display web, console, gui; +// read function +// write function +}; +struct _inputValue +{ + inputField *field; + void *value; // If this is a LUA_TGROUP, then this will be a null. + short valid; // 0 for not yet validated, negative for invalid, positive for valid. + short source, index; +}; - D("For function %s, start of %s validation.", doit, name); - memset((void *) &obj, 0, sizeof(obj)); - fieldValidFuncs->lock(fieldValidFuncs); - while(fieldValidFuncs->getnext(fieldValidFuncs, &obj, NULL, false) == true) - { - char *t = things->getstr(things, obj.name, false); - if (NULL != t) - { - char *nm = obj.name, *v = Rd->valid->getstr(Rd->valid, nm, false); - int valid = 0; - validFunc *vf = (validFunc *) obj.data; - - if (NULL != v) - valid = atoi(v); - - if (0 != valid) // Is it in the valid qhashtbl? - { - if (0 < valid) // Positive valid means it's valid, negative means it's invald. - D("Already validated %s - %s.", vf->title, nm); - else - D("Already invalidated %s - %s.", vf->title, nm); - } - else - { - D("Validating %s - %s.", vf->title, nm); - if (vf->func) - e += vf->func(Rd, things, nm); - else - E("No validation function for %s - %s", vf->title, nm); - } - } - } - fieldValidFuncs->unlock(fieldValidFuncs); - return e; -} - - -static int validateSesh(reqData *Rd, qhashtbl_t *data, char *name) -{ - int ret = 0; - boolean linky = FALSE; - - if ('\0' != Rd->shs.leaf[0]) - { - d("Already validated session."); - return ret; - } - - char *toke_n_munchie = "", *munchie = "", *hashish = "", *leaf = "", *timeStamp = "", *seshion = "", *seshID = "", *t0, *t1; +static int sessionValidate(reqData *Rd, inputForm *iF, inputValue *iV) +{ + int ret = 0; + boolean linky = FALSE; + char *toke_n_munchie = "", *munchie = "", *hashish = "", *leaf = "", *timeStamp = "", *seshion = "", *seshID = "", *t0, *t1; // In this case the session stuff has to come from specific places. hashish = getStrH(Rd->queries, "hashish"); @@ -3265,10 +3575,15 @@ static int validateSesh(reqData *Rd, qhashtbl_t *data, char *name) else { toke_n_munchie = getStrH(Rd->cookies, "toke_n_munchie"); - munchie = getStrH(Rd->body, "munchie"); +// munchie = getStrH(Rd->body, "munchie"); hashish = getStrH(Rd->cookies, "hashish"); if (('\0' == toke_n_munchie[0]) || (('\0' == hashish[0]))) { + if (strcmp("logout", Rd->doit) == 0) + { + d("Not checking session, coz we are logging out."); + return ret; + } bitchSession(Rd, "Invalid session.", "No or blank hashish or toke_n_munchie."); ret++; } @@ -3338,10 +3653,7 @@ static int validateSesh(reqData *Rd, qhashtbl_t *data, char *name) } free(t1); } - } - if (0 == ret) - { if (linky) { t0 = xmprintf("%s%s", getStrH(tnm, "UUID"), munchie); @@ -3359,6 +3671,10 @@ static int validateSesh(reqData *Rd, qhashtbl_t *data, char *name) } free(t1); + } + + if (0 == ret) + { if (now.tv_sec > st.st_mtim.tv_sec + idleTimeOut) { W("Session idled out."); @@ -3384,11 +3700,11 @@ W("Validated session linky."); 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"; + Rd->doit = "validate"; + Rd->output = "accountLogin"; + Rd->form = "accountLogin"; // TODO - we might want to delete their old .lua session as well. Maybe? Don't think we have any suitable codes to find it. } else @@ -3399,13 +3715,16 @@ W("Validated session linky."); 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)); +// TODO - free this somewhere. +// shs->name = tnm->getstr(tnm, "name", true); +// shs->UUID = tnm->getstr(tnm, "UUID", true); 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); } +// TODO - free this somewhere. + shs->name = tnm->getstr(tnm, "name", true); + shs->UUID = tnm->getstr(tnm, "UUID", true); } qhashtbl_obj_t obj; @@ -3416,9 +3735,9 @@ W("Validated session linky."); { char *n = obj.name; - if ((strcmp("salt", n) != 0) && (strcmp("seshID", n) != 0)) + if ((strcmp("salt", n) != 0) && (strcmp("seshID", n) != 0) && (strcmp("UUID", n) != 0)) { -t("Lua %s = %s", n, (char *) obj.data); +t("SessionValidate() Lua read %s = %s", n, (char *) obj.data); Rd->stuff->putstr(Rd->stuff, obj.name, (char *) obj.data); } } @@ -3438,157 +3757,44 @@ t("Lua %s = %s", n, (char *) obj.data); return ret; } -static int validateAboutMe(reqData *Rd, qhashtbl_t *data, char *name) +static void sessionWeb(reqData *Rd, inputForm *iF, inputValue *iV) { - int ret = 0; - char *about = getStrH(data, "aboutMe"); - - if ((strcmp("confirm", Rd->doit) != 0) && (strcmp("update", Rd->doit) != 0)) - return ret; - - if ('\0' == about[0]) - { - bitch(Rd, "Please fill in the 'About me' section.", "None supplied."); - ret++; - } - - badBoy(ret, Rd, data, "aboutMe", about); - return ret; -} - - -char *months[] = -{ - "january", - "february", - "march", - "april", - "may", - "june", - "july", - "august", - "september", - "october", - "november", - "december" -}; -static int validateDoB(reqData *Rd, qhashtbl_t *data, char *name) -{ - int ret = 0, i; - char *t; - - if ((strcmp("confirm", Rd->doit) != 0) && (strcmp("update", Rd->doit) != 0)) - return ret; - - t = getStrH(data, "year"); - if ((NULL == t) || ('\0' == t[0])) - { - bitch(Rd, "Please supply a year of birth.", "None supplied."); - ret++; - } - else - { - i = atoi(t); - if ((1900 > i) || (i > 2020)) - { - bitch(Rd, "Please supply a year of birth.", "Out of range."); - ret++; - } - } - - t = getStrH(data, "month"); - if ((NULL == t) || ('\0' == t[0])) - { - bitch(Rd, "Please supply a month of birth.", "None supplied."); - ret++; - } - else - { - for (i = 0; i < 12; i++) - { - if (strcmp(months[i], t) == 0) - break; - } - if (12 == i) - { - bitch(Rd, "Please supply a month of birth.", "Out of range"); - ret++; - } - } - - badBoy(ret, Rd, data, "month", NULL); - badBoy(ret, Rd, data, "year", NULL); - return ret; + HTMLhidden(Rd->reply, iV->field->name, iV->value); } -static int validateEmail(reqData *Rd, qhashtbl_t *data, char *name) +/* +static int UUIDValidate(reqData *Rd, inputForm *iF, inputValue *iV) { int ret = 0; - char *email = getStrH(data, "email"); - char *emayl = getStrH(data, "emayl"); - - if ((strcmp("confirm", Rd->doit) != 0) && (strcmp("update", Rd->doit) != 0)) - return ret; + char *UUID = (char *) iV->value; - if ((NULL == email) || (NULL == emayl) || ('\0' == email[0]) || ('\0' == emayl[0])) - { - bitch(Rd, "Please supply an email address.", "None supplied."); - ret++; - } - else if (strcmp(email, emayl) != 0) - { - bitch(Rd, "Email addresses are not the same.", ""); - ret++; - } - else if (!qstr_is_email(email)) + if (36 != strlen(UUID)) { - bitch(Rd, "Please supply a proper email address.", "Failed qstr_is_email()"); + bitch(Rd, "Internal error.", "UUID isn't long enough."); ret++; } - else - { -// TODO - do other email checks - does the domain exist, .. - } +// TODO - check the characters and dashes as well. - badBoy(ret, Rd, data, "email", NULL); - badBoy(ret, Rd, data, "emayl", NULL); + if (0 == ret) + Rd->stuff->putstr(Rd->stuff, "UUID", UUID); return ret; } -static int validateLegal(reqData *Rd, qhashtbl_t *data, char *name) +static void UUIDWeb(reqData *Rd, inputForm *iF, inputValue *iV) { - int ret = 0; - char *t; - - t = getStrH(data, "adult"); - if ((NULL == t) || (strcmp("on", t) != 0)) - { - bitch(Rd, "You must be an adult to enter this site.", ""); - ret++; - } - t = getStrH(data, "agree"); - if ((NULL == t) || (strcmp("on", t) != 0)) - { - bitch(Rd, "You must agree to the Terms & Conditions of Use.", ""); - ret++; - } - - badBoy(ret, Rd, data, "adult", NULL); - badBoy(ret, Rd, data, "agree", NULL); - return ret; + HTMLhidden(Rd->reply, iV->field->name, iV->value); } +*/ -static int validateName(reqData *Rd, qhashtbl_t *data, char *nm) +static int nameValidate(reqData *Rd, inputForm *iF, inputValue *iV) { - boolean login = strcmp("login", Rd->doit) == 0; int ret = 0; unsigned char *name; // We have to be unsigned coz of isalnum(). char *where = NULL; - if ((strcmp("cancel", Rd->doit) == 0) || (strcmp("logout", Rd->doit) == 0)) - return ret; +//d("nameValidate %s", iV->field->name); - name = data->getstr(data, "name", true); + name = xstrdup(iV->value); if ((NULL == name) || ('\0' == name[0])) { @@ -3648,350 +3854,469 @@ static int validateName(reqData *Rd, qhashtbl_t *data, char *nm) ret++; break; } +// TODO - compare first, last, and fullname against god names, complain and fail if there's a match. } } - struct stat st; - struct timespec now; - qhashtbl_t *tnm = qhashtbl(0, 0); - int rt = 0; - - if (s) {s--; *s = '_'; s++;} - where = xmprintf("%s/users/%s.lua", scData, name); - rt = LuaToHash(Rd, where, "user", tnm, ret, &st, &now, "user"); - if (s) {s--; *s = '\0'; s++;} - free(where); - - static dbRequest *acnts = NULL; - if (NULL == acnts) - { - static char *szi[] = {"FirstName", "LastName", NULL}; - static char *szo[] = {NULL}; - acnts = xzalloc(sizeof(dbRequest)); - acnts->db = Rd->db; - acnts->table = "UserAccounts"; - acnts->inParams = szi; - acnts->outParams = szo; - acnts->where = "FirstName=? and LastName=?"; - dbRequests->addfirst(dbRequests, acnts, sizeof(*acnts)); - } - dbDoSomething(acnts, FALSE, name, s); - rowData *rows = acnts->rows; - int c = 0; - - if (rows) - c = rows->rows->size(rows->rows); - - if (login) - { - if ((1 != c) && (rt)) - { - if (rt) - { - W("Could not read user Lua file."); - ret += rt; - } - if (0 == c) - { - W("No UserAccounts record with that name."); - ret++; - } - else - { - W("More than one UserAccounts record with that name."); - ret++; - } - bitch(Rd, "Login failed.", "Could not find user record."); - } - else - { - if (1 == c) - dbPull(Rd, "UserAccounts", rows); - else - { - Rd->database->putstr(Rd->database, "UserAccounts.FirstName", name); - Rd->database->putstr(Rd->database, "UserAccounts.LastName", s); - Rd->database->putstr(Rd->database, "UserAccounts.Email", getStrH(tnm, "email")); - Rd->database->putstr(Rd->database, "UserAccounts.Created", getStrH(tnm, "created")); - Rd->database->putstr(Rd->database, "UserAccounts.PrincipleID", getStrH(tnm, "UUID")); - Rd->database->putstr(Rd->database, "UserAccounts.UserLevel", getStrH(tnm, "level")); - Rd->database->putstr(Rd->database, "UserAccounts.UserFlags", getStrH(tnm, "flags")); - Rd->database->putstr(Rd->database, "UserAccounts.UserTitle", getStrH(tnm, "title")); - Rd->database->putstr(Rd->database, "UserAccounts.active", getStrH(tnm, "active")); - Rd->database->putstr(Rd->database, "auth.passwordSalt", getStrH(tnm, "passwordSalt")); - Rd->database->putstr(Rd->database, "auth.passwordHash", getStrH(tnm, "passwordHash")); - tnm->free(tnm); - } - Rd->stuff->putstr(Rd->stuff, "UUID", getStrH(Rd->database, "UserAccounts.PrincipalID")); - Rd->stuff->putstr(Rd->stuff, "level", getStrH(Rd->database, "UserAccounts.Userlevel")); - if (s) {s--; *s = ' '; s++;} - Rd->stuff->putstr(Rd->stuff, "name", name); - if (s) {s--; *s = '\0'; s++;} - } - } - else if (strcmp("create", Rd->doit) == 0) + if (0 == ret) { - if ((0 != c) && (!rt)) - { - bitch(Rd, "Pick a different name.", "An existing Lua user file or UserAccounts record matched that name."); - ret++; - } - else - { -// TODO - compare first, last, and fullname against god names, complain and fail if there's a match. - // Generate a UUID, check it isn't already being used. - char uuid[37]; - uuid_t binuuid; - my_ulonglong users = 0; - - do // UserAccounts.PrincipalID is a unique primary index anyway, but we want the user creation process to be a little on the slow side. - { - struct stat st; - - uuid_generate_random(binuuid); - uuid_unparse_lower(binuuid, uuid); - // Try Lua user file. - where = xmprintf("%s/users/%s.lua", scData, uuid); - c = stat(where, &st); - if (c) - users = 1; - free(where); - // Try database. - where = xmprintf("UserAccounts.PrincipalID = '%s'", uuid); - D("Trying new UUID %s.", where); - users = dbCount(Rd->db, "UserAccounts", where); - free(where); - } while (users != 0); -// TODO - perhaps create a place holder UserAccounts record? Then we'll have to deal with deleting them later. - Rd->stuff->putstr(Rd->stuff, "UUID", xstrdup(uuid)); - Rd->stuff->putstr(Rd->stuff, "level", xstrdup("-200")); - Rd->database->putstr(Rd->database, "UserAccounts.PrincipalID",xstrdup(uuid)); - Rd->database->putstr(Rd->database, "UserAccounts.Userlevel", xstrdup("-200")); - Rd->database->putstr(Rd->database, "UserAccounts.firstName", xstrdup(name)); - Rd->database->putstr(Rd->database, "UserAccounts.lastName", xstrdup(s)); - if (s) {s--; *s = ' '; s++;} - Rd->stuff->putstr(Rd->stuff, "name", xstrdup(name)); - } + Rd->stuff->putstr(Rd->stuff, "firstName", name); + Rd->stuff->putstr(Rd->stuff, "lastName", s); + if (s) {s--; *s = ' '; s++;} + s = NULL; + Rd->stuff->putstr(Rd->stuff, "name", name); + Rd->shs.name = xstrdup(name); } - free(rows->fieldNames); - rows->rows->free(rows->rows); - free(rows); - tnm->free(tnm); if (s) {s--; *s = ' '; s++;} } } free(name); - badBoy(ret, Rd, data, "name", NULL); return ret; } -static int validatePassword(reqData *Rd, qhashtbl_t *data, char *name) +static void nameWeb(reqData *Rd, inputForm *oF, inputValue *oV) +{ + if (oV->field->flags & FLD_HIDDEN) + HTMLhidden(Rd->reply, oV->field->name, oV->value); + else + HTMLtext(Rd->reply, "text", oV->field->title, oV->field->name, oV->value, oV->field->viewLength, oV->field->maxLength, oV->field->flags & FLD_REQUIRED); +} + + +static int passwordValidate(reqData *Rd, inputForm *iF, inputValue *iV) { - boolean login = strcmp("login", Rd->doit) == 0; - boolean create = strcmp("create", Rd->doit) == 0; int ret = 0; - char *password = getStrH(data, "password"); - char *psswrdH = getStrH(Rd->stuff, "passwordHash"); - char *psswrdS = getStrH(Rd->stuff, "passwordSalt"); + char *password = (char *) iV->value, *salt = getStrH(Rd->stuff, "passSalt"), *hash = getStrH(Rd->stuff, "passHash"); + + if ((NULL == password) || ('\0' == password[0])) + { + bitch(Rd, "Please supply a password.", "Password empty or missing."); + ret++; + } + else if (('\0' != salt[0]) && ('\0' != hash[0]) && (strcmp("psswrd", iV->field->name) == 0)) + { + D("Comparing passwords. %s %s %s", password, salt, hash); + char *h = checkSLOSpassword(Rd, salt, password, hash, "Passwords are not the same."); + + if (NULL == h) + ret++; + else + free(h); + } + +// TODO - once the password is validated, store it as the salt and hash. +// If it's an existing account, compare it? Or do that later? + if (0 == ret) + Rd->stuff->putstr(Rd->stuff, "password", password); + + return ret; +} + +static void passwordWeb(reqData *Rd, inputForm *oF, inputValue *oV) +{ + HTMLtext(Rd->reply, "password", oV->field->title, oV->field->name, "", oV->field->viewLength, oV->field->maxLength, oV->field->flags & FLD_REQUIRED); + Rd->reply->addstr(Rd->reply, "

While viewers will usually remember your name and password for you, you'll need to remember it for this web site to.   " + "I highly recommend using a password manager.   KeePass and it's variations is a great password manager.

\n"); +} + +static int emailValidate(reqData *Rd, inputForm *iF, inputValue *iV) +{ +// inputField **group = iV->field->group; + int ret = 0, i; + boolean notSame = FALSE; - if (login) + i = iV->index; + if (2 == i) { - char *UUID = getStrH(Rd->database, "UserAccounts.PrincipalID"); - static dbRequest *auth = NULL; + char *email = (char *) iV->value; + char *emayl = (char *) (iV + 1)->value; - if ('\0' == UUID[0]) // If the name validation didn't find us a UUID, then this user doesn't exist, so can't log them on. - return 1; - if (NULL == auth) + if ((NULL == email) || (NULL == emayl) || ('\0' == email[0]) || ('\0' == emayl[0])) + { + bitch(Rd, "Please supply an email address.", "None supplied."); + ret++; + } + else if (strcmp(email, emayl) != 0) + { + bitch(Rd, "Email addresses are not the same.", ""); + ret++; + notSame = TRUE; + } + else if (!qstr_is_email(email)) { - static char *szi[] = {"UUID", NULL}; - static char *szo[] = {"passwordSalt", "passwordHash", NULL}; - auth = xzalloc(sizeof(dbRequest)); - auth->db = Rd->db; - auth->table = "auth"; - auth->inParams = szi; - auth->outParams = szo; - auth->where = "UUID=?"; - dbRequests->addfirst(dbRequests, auth, sizeof(*auth)); + bitch(Rd, "Please supply a proper email address.", "Failed qstr_is_email()"); + ret++; } - dbDoSomething(auth, FALSE, UUID); - rowData *rows = auth->rows; - if (rows) + else { - int i = rows->rows->size(rows->rows); +// TODO - do other email checks - does the domain exist, .. + } - 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 ((NULL != email) && (NULL != emayl)) + { + char *t0 = qurl_encode(email, strlen(email)); - 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")); + // In theory it's the correct thing to do to NOT load email into stuff on failure, + // In practice, that means it wont show the old email and emayl in the create page when they don't match. + if ((0 == ret) || notSame) + Rd->stuff->putstrf(Rd->stuff, "email", "%s", t0); + free(t0); + } + if ((NULL != email) && (NULL != emayl)) + { + char *t1 = qurl_encode(emayl, strlen(emayl)); - char *md5ascii = qhex_encode(md5hash, 16); - char *where = xmprintf("%s:%s", md5ascii, getStrH(me, "passwordSalt")); - if (!qhashmd5((void *) where, strlen(where), md5hash)) - { - bitch(Rd, "Login failed, internal error.", "Login - qhashmd5(passwordSalt) failed."); - ret++; - } - else - { - free(md5ascii); - md5ascii = qhex_encode(md5hash, 16); - if (strcmp(md5ascii, getStrH(me, "passwordHash")) != 0) - { - bitch(Rd, "Login failed.", "passwordHash doesn't match"); - ret++; - } - Rd->stuff->putstr(Rd->stuff, "passwordHash", md5ascii); - } - free(md5ascii); - free(where); - } - free(me); - } - free(rows->fieldNames); - rows->rows->free(rows->rows); - free(rows); + Rd->stuff->putstrf(Rd->stuff, "emayl", "%s", t1); + free(t1); } } - else if (create) + + return ret; +} +static void emailWeb(reqData *Rd, inputForm *oF, inputValue *oV) +{ + HTMLtext(Rd->reply, "email", oV->field->title, oV->field->name, displayPrep(getStrH(Rd->stuff, oV->field->name)), oV->field->viewLength, oV->field->maxLength, oV->field->flags & FLD_REQUIRED); +} + + +char *months[] = +{ + "january", + "february", + "march", + "april", + "may", + "june", + "july", + "august", + "september", + "october", + "november", + "december" +}; +static int DoBValidate(reqData *Rd, inputForm *iF, inputValue *iV) +{ + int ret = 0, i; + char *t0, *t1; +// inputField **group = iV->field->group; + + i = iV->index; + if (2 == i) { - if ((NULL == password) || ('\0' == password[0])) + t0 = (char *) iV->value; + if ((NULL == t0) || ('\0' == t0[0])) { - bitch(Rd, "Please supply a password.", "Password empty or missing."); + bitch(Rd, "Please supply a year of birth.", "None supplied."); 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)) + i = atoi(t0); + if ((1900 > i) || (i > 2020)) { - bitch(Rd, "Internal session error.", "Create - qhashmd5(new uuid) failed."); + bitch(Rd, "Please supply a year of birth.", "Out of range."); ret++; } - else - { - salt = qhex_encode(md5hash, 16); - Rd->stuff->putstr(Rd->stuff, "passwordSalt", salt); - if (!qhashmd5((void *) password, strlen(password), md5hash)) - { - bitch(Rd, "Internal session error.", "Create - qhashmd5(password) failed."); - ret++; - } - else - { - free(salt); - salt = qhex_encode(md5hash, 16); - hash = xmprintf("%s:%s", salt, getStrH(Rd->stuff, "passwordSalt")); - if (!qhashmd5((void *) hash, strlen(hash), md5hash)) - { - bitch(Rd, "Internal session error.", "Create - qhashmd5(passwordSalt) failed."); - ret++; - } - else - { - free(hash); - hash = qhex_encode(md5hash, 16); - Rd->stuff->putstr(Rd->stuff, "passwordHash", hash); - Rd->chillOut = TRUE; - } - free(hash); - free(salt); - } - } } - } - else if (strcmp("confirm", Rd->doit) == 0) - { - if ((NULL == password) || ('\0' == password[0])) + t1 = (char *) (iV + 1)->value; + if ((NULL == t1) || ('\0' == t1[0])) { - bitch(Rd, "Please supply a password.", "Need two passwords."); + bitch(Rd, "Please supply a month of birth.", "None supplied."); 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)) + for (i = 0; i < 12; i++) { - bitch(Rd, "Internal session error.", "Confirm - qhashmd5(password) failed."); - ret++; + if (strcmp(months[i], t1) == 0) + break; } - else + if (12 == i) { - pswd = qhex_encode(md5hash, 16); - hash = xmprintf("%s:%s", pswd, Osalt); - free(pswd); - if (!qhashmd5((void *) hash, strlen(hash), md5hash)) - { - bitch(Rd, "Internal session error.", "Confirm - qhashmd5(passwordSalt) failed."); - ret++; - } - else - { - free(hash); - hash = qhex_encode(md5hash, 16); - if (strcmp(hash, Ohash) != 0) - { - bitch(Rd, "Passwords are not the same.", ""); - ret++; - } - free(hash); - } + bitch(Rd, "Please supply a month of birth.", "Out of range"); + ret++; } } + + if (0 == ret) + { + Rd->stuff->putstr(Rd->stuff, "year", t0); + Rd->stuff->putstr(Rd->stuff, "month", t1); + Rd->stuff->putstrf(Rd->stuff, "DoB", "%s %s", t0, t1); + } } -// TODO - try to find code for dealing with security enclaves, encrypted memory, and such. -// NOTE - these get filtered through what ever web server is being used, and might leak there. - OPENSSL_cleanse(password, strlen(password)); + return ret; +} +static void DoByWeb(reqData *Rd, inputForm *oF, inputValue *oV) +{ + char *tmp = xmalloc(16), *t; + int i, d; - badBoy(ret, Rd, data, "auth.passwordSalt", NULL); + Rd->reply->addstr(Rd->reply, "\n"); +} +static void DoBWeb(reqData *Rd, inputForm *oF, inputValue *oV) +{ +} + +static int legalValidate(reqData *Rd, inputForm *iF, inputValue *iV) { int ret = 0, i; - char uuid[37], *t; + char *t; + inputField **group = iV->field->group; + + i = iV->index; + if (2 == i) + { + t = (char *) iV->value; + if ((NULL == t) || (strcmp("on", t) != 0)) + { + bitch(Rd, "You must be an adult to enter this site.", ""); + ret++; + } + else + Rd->stuff->putstr(Rd->stuff, "adult", t); + t = (char *) (iV + 1)->value; + if ((NULL == t) || (strcmp("on", t) != 0)) + { + bitch(Rd, "You must agree to the Terms & Conditions of Use.", ""); + ret++; + } + else + Rd->stuff->putstr(Rd->stuff, "agree", t); + } + + return ret; +} +static void adultWeb(reqData *Rd, inputForm *oF, inputValue *oV) +{ + HTMLcheckBox(Rd->reply, oV->field->name, oV->field->title, !strcmp("on", getStrH(Rd->body, "adult")), oV->field->flags & FLD_REQUIRED); +} +static void agreeWeb(reqData *Rd, inputForm *oF, inputValue *oV) +{ + HTMLcheckBox(Rd->reply, oV->field->name, oV->field->title, !strcmp("on", getStrH(Rd->body, "agree")), oV->field->flags & FLD_REQUIRED); +} +static void legalWeb(reqData *Rd, inputForm *oF, inputValue *oV) +{ +} +static void ToSWeb(reqData *Rd, inputForm *oF, inputValue *oV) +{ + Rd->reply->addstrf(Rd->reply, "

Terms of Service

%s
\n", getStrH(Rd->configs, "ToS")); +} + +static int aboutMeValidate(reqData *Rd, inputForm *oF, inputValue *oV) +{ + int ret = 0; + char *about = (char *) oV->value; + + if ((NULL == about) || ('\0' == about[0])) + { + bitch(Rd, "Please fill in the 'About me' section.", "None supplied."); + ret++; + } + + if ((0 == ret) && (NULL != about)) + { + char *t = qurl_encode(about, strlen(about)); + Rd->stuff->putstr(Rd->stuff, "aboutMe", t); + free(t); + } + + return ret; +} + +static void aboutMeWeb(reqData *Rd, inputForm *oF, inputValue *oV) +{ + // For maxlength - the MySQL database field is type text, which has a max length of 64 Kilobytes byets, but characters might take up 1 - 4 bytes, and maxlength is in characters. + // For rows and cols, seems a bit broken, I ask for 5/42, I get 6,36. In world it seems to be 7,46 +// TODO - check against the limit for in world profiles, coz this will become that. +// TODO - validate aboutMe, it should not be empty, and should not be longer than 64 kilobytes. + HTMLtextArea(Rd->reply, oV->field->name, oV->field->title, 7, oV->field->viewLength, 4, oV->field->maxLength, "Describe yourself here.", "off", "true", "soft", oV->value, FALSE, FALSE); +} + +static void accountWebHeaders(reqData *Rd, inputForm *oF, char *name) +{ + char *linky = checkLinky(Rd); + + HTMLheader(Rd->reply, " account manager"); + Rd->reply->addstrf(Rd->reply, "

account manager

\n"); + if (NULL != name) + { + Rd->reply->addstrf(Rd->reply, "

account for %s

\n", name); + Rd->reply->addstr(Rd->reply, linky); + } + free(linky); + if (0 != Rd->errors->size(Rd->messages)) + HTMLlist(Rd->reply, "messages -", Rd->messages); + if (NULL != oF->help) + Rd->reply->addstrf(Rd->reply, "

%s

\n", oF->help); + HTMLform(Rd->reply, "", Rd->shs.munchie); + HTMLhidden(Rd->reply, "form", oF->name); +} + +static void accountWebFields(reqData *Rd, inputForm *oF, inputValue *oV) +{ + int count = oF->fields->size(oF->fields), i; + + for (i = 0; i < count; i++) + { + if (NULL != oV[i].field->web) + oV[i].field->web(Rd, oF, &oV[i]); + if ((NULL != oV[i].field->help) && ('\0' != oV[i].field->help[0])) + Rd->reply->addstrf(Rd->reply, "

%s

\n", oV[i].field->help); +//d("accountWebFeilds(%s, %s)", oF->name, oV[i].field->name); + } +} + +static void accountWebSubs(reqData *Rd, inputForm *oF) +{ + qhashtbl_obj_t obj; + + Rd->reply->addstrf(Rd->reply, "\n"); // Stop Enter key on text fields triggering the first submit button. + memset((void*)&obj, 0, sizeof(obj)); // must be cleared before call + oF->subs->lock(oF->subs); + while(oF->subs->getnext(oF->subs, &obj, false) == true) + { + inputSub *sub = (inputSub *) obj.data; + if ('\0' != sub->title[0]) + HTMLbutton(Rd->reply, sub->title); +//d("accountWebSubs(%s, %s '%s')", oF->name, sub->name, sub->title); + } + oF->subs->unlock(oF->subs); +} + +static void accountWebFooter(reqData *Rd, inputForm *oF) +{ + if (0 != Rd->errors->size(Rd->errors)) + HTMLlist(Rd->reply, "errors -", Rd->errors); + HTMLformEnd(Rd->reply); + HTMLfooter(Rd->reply); +} + +static void accountAddWeb(reqData *Rd, inputForm *oF, inputValue *oV) +{ + char *name = getStrH(Rd->stuff, "name"); + + accountWebHeaders(Rd, oF, name); + accountWebFields(Rd, oF, oV); + accountWebSubs(Rd, oF); + accountWebFooter(Rd, oF); +} + +static void accountLoginWeb(reqData *Rd, inputForm *oF, inputValue *oV) +{ + char *name = getStrH(Rd->stuff, "name"); + + Rd->shs.UUID = NULL; + accountWebHeaders(Rd, oF, NULL); + accountWebFields(Rd, oF, oV); + accountWebSubs(Rd, oF); + accountWebFooter(Rd, oF); +} + +static void accountViewWeb(reqData *Rd, inputForm *oF, inputValue *oV) +{ + time_t crtd = atol(getStrH(Rd->database, "UserAccounts.Created")); + + accountWebHeaders(Rd, oF, getStrH(Rd->stuff, "name")); + accountWebFields(Rd, oF, oV); +// TODO - still need to encode < > as < u> for email and about. +// TODO - dammit, qurl_decode returns the string length, and decodes the string in place. + Rd->reply->addstrf(Rd->reply, "

Title / level : %s / %d

", getLevel(Rd), Rd->shs.level); + Rd->reply->addstrf(Rd->reply, "

Date of birth : %s

", getStrH(Rd->database, "Lua.DoB")); + Rd->reply->addstrf(Rd->reply, "

Created : %s

", ctime(&crtd)); + Rd->reply->addstrf(Rd->reply, "

Email : %s

", displayPrep(getStrH(Rd->stuff, "email"))); + Rd->reply->addstrf(Rd->reply, "

UUID : %s

", Rd->shs.UUID); +// Rd->reply->addstrf(Rd->reply, "

About :

" +// "", qurl_decode(getStrH(Rd->database, "Lua.aboutMe"))); + HTMLtextArea(Rd->reply, "aboutMe", "About", 7, 50, 4, 16384, "", "off", "true", "soft", displayPrep(getStrH(Rd->database, "Lua.aboutMe")), FALSE, TRUE); + accountWebSubs(Rd, oF); + accountWebFooter(Rd, oF); +} + +static void accountEditWeb(reqData *Rd, inputForm *oF, inputValue *oV) +{ + char *name = getStrH(Rd->stuff, "name"); + + accountWebHeaders(Rd, oF, name); + accountWebFields(Rd, oF, oV); + HTMLtext(Rd->reply, "password", "Old password", "password", "", 16, 0, FALSE); + Rd->reply->addstr(Rd->reply, "

Warning, the limit on password length is set by your viewer, some can't handle longer than 16 characters.

\n"); +//// HTMLtext(Rd->reply, "title", "text", "title", getStrH(Rh->stuff, "title"), 16, 64, TRUE); + + qlisttbl_obj_t obj; + char *lvl = getLevel(Rd); + + HTMLselect(Rd->reply, "level", "level"); + memset((void*)&obj, 0, sizeof(obj)); // must be cleared before call + accountLevels->lock(accountLevels); + while(accountLevels->getnext(accountLevels, &obj, NULL, false) == true) + { + boolean is = false; + + if (strcmp(lvl, (char *) obj.data) == 0) + is = true; + HTMLoption(Rd->reply, (char *) obj.data, is); + } + accountLevels->unlock(accountLevels); + HTMLselectEnd(Rd->reply); + + accountWebSubs(Rd, oF); + accountWebFooter(Rd, oF); +} + + +static int accountRead(reqData *Rd, inputForm *iF, inputValue *iV) +{ + int ret = 0, rt = -1; + struct stat st; + struct timespec now; + qhashtbl_t *tnm = qhashtbl(0, 0); + char *uuid, *first, *last; + uuid_t binuuid; rowData *rows = NULL; - static dbRequest *uuids = NULL; + // Setup the database stuff. + static dbRequest *uuids = NULL; if (NULL == uuids) { static char *szi[] = {"PrincipalID", NULL}; @@ -4004,249 +4329,584 @@ static int validateUUID(reqData *Rd, qhashtbl_t *data, char *name) uuids->where = "PrincipalID=?"; dbRequests->addfirst(dbRequests, uuids, sizeof(*uuids)); } + static dbRequest *acnts = NULL; + if (NULL == acnts) + { + static char *szi[] = {"FirstName", "LastName", NULL}; + static char *szo[] = {NULL}; + acnts = xzalloc(sizeof(dbRequest)); + acnts->db = Rd->db; + acnts->table = "UserAccounts"; + acnts->inParams = szi; + acnts->outParams = szo; + acnts->where = "FirstName=? and LastName=?"; + dbRequests->addfirst(dbRequests, acnts, sizeof(*acnts)); + } + 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=?"; + dbRequests->addfirst(dbRequests, auth, sizeof(*auth)); + } - if ((strcmp("cancel", Rd->doit) == 0) || (strcmp("logout", Rd->doit) == 0)) - return ret; + uuid = Rd->shs.UUID; first = getStrH(Rd->stuff, "firstName"); last = getStrH(Rd->stuff, "lastName"); +d("accountRead() UUID %s, name %s %s", uuid, first, last); + uuid_clear(binuuid); + if ((NULL != uuid) && ('\0' != uuid[0])) + uuid_parse(uuid, binuuid); + if ((NULL != uuid) && ('\0' != uuid[0]) && (!uuid_is_null(binuuid))) + { + char *where = xmprintf("%s/users/%s.lua", scData, uuid); + rt = LuaToHash(Rd, where, "user", tnm, ret, &st, &now, "user"); - uuid[0] = '\0'; - if (strcmp("create", Rd->doit) == 0) + free(where); + dbDoSomething(uuids, FALSE, uuid); + rows = uuids->rows; + } + else { - // Generate a UUID, check it isn't already being used, and totally ignore whatever UUID is in body. - uuid_t binuuid; - do // UserAccounts.PrincipalID is a unique primary index anyway, but we want the user creation process to be a little on the slow side. + if ('\0' != first[0]) { - uuid_generate_random(binuuid); - uuid_unparse_lower(binuuid, uuid); + char *where = xmprintf("%s/users/%s_%s.lua", scData, first, last); + rt = LuaToHash(Rd, where, "user", tnm, ret, &st, &now, "user"); - d("Trying new UUID %s.", uuid); -// TODO - check the Lua user files as well. - dbDoSomething(uuids, FALSE, uuid); - rows = uuids->rows; - i = 0; + free(where); + dbDoSomething(acnts, FALSE, first, last); + rows = acnts->rows; + } + } +// else +// { +// bitch(Rd, "Unable to read user record.", "Nothing available to look up a user record with."); +// rt = 1; +// } + + if (0 == rt) + { + ret += 1; + Rd->database->putstr(Rd->database, "UserAccounts.FirstName", first); + Rd->database->putstr(Rd->database, "UserAccounts.LastName", last); + Rd->database->putstr(Rd->database, "UserAccounts.Email", getStrH(tnm, "email")); + Rd->database->putstr(Rd->database, "UserAccounts.Created", getStrH(tnm, "created")); + Rd->database->putstr(Rd->database, "UserAccounts.PrincipalID", getStrH(tnm, "UUID")); + Rd->database->putstr(Rd->database, "UserAccounts.UserLevel", getStrH(tnm, "level")); + Rd->database->putstr(Rd->database, "UserAccounts.UserFlags", getStrH(tnm, "flags")); + Rd->database->putstr(Rd->database, "UserAccounts.UserTitle", getStrH(tnm, "title")); + Rd->database->putstr(Rd->database, "UserAccounts.active", getStrH(tnm, "active")); + Rd->database->putstr(Rd->database, "auth.passwordSalt", getStrH(tnm, "passwordSalt")); + Rd->database->putstr(Rd->database, "auth.passwordHash", getStrH(tnm, "passwordHash")); + Rd->stuff-> putstr(Rd->stuff, "linky-hashish", getStrH(tnm, "linky-hashish")); + Rd->database->putstr(Rd->database, "Lua.name", getStrH(tnm, "name")); + Rd->database->putstr(Rd->database, "Lua.DoB", getStrH(tnm, "DoB")); + Rd->database->putstr(Rd->database, "Lua.agree", getStrH(tnm, "agree")); + Rd->database->putstr(Rd->database, "Lua.adult", getStrH(tnm, "adult")); + Rd->database->putstr(Rd->database, "Lua.aboutMe", getStrH(tnm, "aboutMe")); + Rd->database->putstr(Rd->database, "Lua.vouched", getStrH(tnm, "vouched")); + } + else if (rows) + { + ret += rows->rows->size(rows->rows); + if (1 == ret) + { + dbPull(Rd, "UserAccounts", rows); + dbDoSomething(auth, FALSE, getStrH(Rd->database, "UserAccounts.PrincipalID")); + rows = auth->rows; if (rows) - i = rows->rows->size(rows->rows); - else { - bitch(Rd, "Internal error.", "Matching UUID record found in UserAccounts."); - ret++; - break; + if (1 == rows->rows->size(rows->rows)) + dbPull(Rd, "auth", rows); } - } while (i != 0); - if (0 == ret) - { - data->putstr(data, "UUID", xstrdup(uuid)); - Rd->stuff->putstr(Rd->stuff, "UUID", xstrdup(uuid)); } - rows = NULL; } - else if ((strcmp("confirm", Rd->doit) == 0) || (strcmp("logout", Rd->doit) == 0)) + else + { + d("No user name or UUID to get an account for."); + } + + if (1 == ret) + { +// TODO - this has to change when we are editing other peoples accounts. + Rd->shs.UUID = Rd->database->getstr(Rd->database, "UserAccounts.PrincipalID", true); + Rd->stuff->putstr(Rd->stuff, "email", getStrH(Rd->database, "UserAccounts.Email")); + Rd->shs.level = atoi(getStrH(Rd->database, "UserAccounts.UserLevel")); + } + + tnm->free(tnm); + return ret; +} + +static int accountDel(reqData *Rd, inputForm *iF, inputValue *iV) +{ + int ret = 0; + int c = accountRead(Rd, iF, iV); + + if (1 != c) + { + bitch(Rd, "Cannot delete account.", "Account doesn't exist."); + ret++; + } + else + { +// check if logged in user is allowed to delete this account +// delete user record +// log the user out if they are logged in + } + return ret; +} +static int accountCreate(reqData *Rd, inputForm *iF, inputValue *iV) +{ + int ret = 0; + int c = accountRead(Rd, iF, iV); + boolean wipe = FALSE; + + if (strcmp("POST", Rd->Method) == 0) { - t = getStrH(data, "UUID"); - if (36 != strlen(t)) + if (0 != c) { - bitch(Rd, "Internal error.", "UUID isn't long enough."); + bitch(Rd, "Cannot create account.", "Account exists."); ret++; } else - strcpy(uuid, t); + { + char *salt = newSLOSsalt(Rd); + char *h = checkSLOSpassword(Rd, salt, getStrH(Rd->body, "password"), NULL, NULL); + + if (NULL == h) + ret++; + else + { + Rd->stuff->putstr(Rd->stuff, "passHash", h); + Rd->stuff->putstr(Rd->stuff, "passSalt", salt); + free(h); + } + free(salt); + if (0 != ret) + { + wipe = TRUE; + Rd->shs.UUID = NULL; + Rd->output = "accountLogin"; + } + } } - else + freeSesh(Rd, FALSE, wipe); + newSesh(Rd, FALSE); + return ret; +} +static int accountAdd(reqData *Rd, inputForm *iF, inputValue *iV) +{ + int ret = 0; + int c = accountRead(Rd, iF, iV); + boolean wipe = FALSE; + + if (0 != c) { - if ('\0' != getStrH(Rd->database, "UserAccounts.ScopeID")[0]) return ret; + bitch(Rd, "Cannot add account.", "Account exists."); + ret++; + } + else if ((0 == ret) && (strcmp("POST", Rd->Method) == 0)) + { + char *h = checkSLOSpassword(Rd, getStrH(Rd->stuff, "passSalt"), getStrH(Rd->stuff, "password"), getStrH(Rd->stuff, "passHash"), "Passwords are not the same."); - t = getStrH(data, "UUID"); - if (36 != strlen(t)) + if (NULL == h) { - bitch(Rd, "Internal error.", "UUID isn't long enough."); ret++; + wipe = TRUE; + Rd->shs.UUID = NULL; + Rd->output = "accountLogin"; } else { - strcpy(uuid, t); - dbDoSomething(uuids, FALSE, uuid); - rows = uuids->rows; - if (rows) + free(h); + generateAccountUUID(Rd); + Rd->stuff->putstr(Rd->stuff, "passwordHash", getStrH(Rd->stuff, "passHash")); + Rd->stuff->putstr(Rd->stuff, "passwordSalt", getStrH(Rd->stuff, "passSalt")); + Rd->shs.level = -200; + freeSesh(Rd, FALSE, wipe); + newSesh(Rd, TRUE); + accountWrite(Rd); +// log them in + I("Logged on %s %s Level %d %s", Rd->shs.UUID, getStrH(Rd->stuff, "name"), Rd->shs.level, getLevel(Rd)); + Rd->output = "accountView"; + Rd->form = "accountView"; + Rd->doit = "login"; +// TODO - send email. Quick and easy is to invoke the sandmail command. Later use libcurl. + } + } + freeSesh(Rd, FALSE, wipe); + newSesh(Rd, FALSE); + return ret; +} + +static int accountSave(reqData *Rd, inputForm *iF, inputValue *iV) +{ + int ret = 0; + int c = accountRead(Rd, iF, iV); + boolean wipe = FALSE; + + if (1 != c) + { + bitch(Rd, "Cannot save account.", "Account doesn't exist."); + ret++; + } + else if ((0 == ret) && (strcmp("POST", Rd->Method) == 0)) + { + char *h = checkSLOSpassword(Rd, getStrH(Rd->stuff, "passSalt"), getStrH(Rd->body, "password"), getStrH(Rd->stuff, "passHash"), "Passwords are not the same."); + if (NULL == h) + { + ret++; + wipe = TRUE; + Rd->shs.UUID = NULL; + Rd->output = "accountLogin"; + } + else + { + free(h); + Rd->stuff->putstr(Rd->stuff, "passwordHash", getStrH(Rd->stuff, "passHash")); + Rd->stuff->putstr(Rd->stuff, "passwordSalt", getStrH(Rd->stuff, "passSalt")); + accountWrite(Rd); + } + } + freeSesh(Rd, FALSE, wipe); + newSesh(Rd, FALSE); + return ret; +} + +static int accountValidate(reqData *Rd, inputForm *iF, inputValue *iV) +{ + int ret = 0; + int c = accountRead(Rd, iF, iV); + boolean wipe = FALSE; + + if (1 != c) + { + bitch(Rd, "Cannot validate account.", "Account doesn't exist."); + ret++; + } + else + { + Rd->stuff->putstr(Rd->stuff, "email", getStrH(Rd->database, "UserAccounts.Email")); + Rd->stuff->putstr(Rd->stuff, "created", getStrH(Rd->database, "UserAccounts.Created")); + Rd->stuff->putstr(Rd->stuff, "flags", getStrH(Rd->database, "UserAccounts.UserFlags")); + Rd->stuff->putstr(Rd->stuff, "active", getStrH(Rd->database, "UserAccounts.active")); + Rd->stuff->putstr(Rd->stuff, "passwordSalt", getStrH(Rd->database, "auth.passwordSalt")); + Rd->stuff->putstr(Rd->stuff, "passwordHash", getStrH(Rd->database, "auth.passwordHash")); + Rd->stuff->putstr(Rd->stuff, "name", getStrH(Rd->database, "Lua.name")); + Rd->stuff->putstr(Rd->stuff, "DoB", getStrH(Rd->database, "Lua.DoB")); + Rd->stuff->putstr(Rd->stuff, "agree", getStrH(Rd->database, "Lua.agree")); + Rd->stuff->putstr(Rd->stuff, "adult", getStrH(Rd->database, "Lua.adult")); + Rd->stuff->putstr(Rd->stuff, "aboutMe", getStrH(Rd->database, "Lua.aboutMe")); + Rd->stuff->putstr(Rd->stuff, "vouched", getStrH(Rd->database, "Lua.vouched")); + Rd->shs.level = -100; + accountWrite(Rd); + wipe = TRUE; + } + freeSesh(Rd, FALSE, wipe); + newSesh(Rd, FALSE); + return ret; +} + + +static int accountView(reqData *Rd, inputForm *iF, inputValue *iV) +{ +// TODO - this has to change when we are editing other peoples accounts. + int ret = 0; + int c = accountRead(Rd, iF, iV); + boolean wipe = FALSE; + +d("Sub accountView %s %s %s", getStrH(Rd->database, "UserAccounts.PrincipalID"), getStrH(Rd->database, "UserAccounts.FirstName"), getStrH(Rd->database, "UserAccounts.LastName")); + if (1 != c) + { + bitch(Rd, "Cannot view account.", "Account doesn't exist."); + ret++; + wipe = TRUE; + Rd->shs.UUID = NULL; + Rd->output = "accountLogin"; + } + else + { + // Check password on POST if the session user is the same as the shown user, coz this is the page shown on login. + if ((strcmp("POST", Rd->Method) == 0) && (strcmp(Rd->shs.UUID, getStrH(Rd->database, "UserAccounts.PrincipalID")) == 0)) + { + char *h = checkSLOSpassword(Rd, getStrH(Rd->database, "auth.passwordSalt"), getStrH(Rd->body, "password"), getStrH(Rd->database, "auth.passwordHash"), "Login failed."); + if (NULL == h) { - if (1 != rows->rows->size(rows->rows)) - { - bitch(Rd, "Internal error.", "No matching UUID record found in UserAccounts."); - ret++; - } + ret++; + wipe = TRUE; + Rd->shs.UUID = NULL; + Rd->output = "accountLogin"; + } + else + { + free(h); + I("Logged on %s %s Level %d %s", Rd->shs.UUID, getStrH(Rd->stuff, "name"), Rd->shs.level, getLevel(Rd)); } } } - - if (!badBoy(ret, Rd, data, "UUID", uuid)) + freeSesh(Rd, FALSE, wipe); + newSesh(Rd, FALSE); + + return ret; +} +static int accountEdit(reqData *Rd, inputForm *iF, inputValue *iV) +{ + int ret = 0; + int c = accountRead(Rd, iF, iV); + +d("Sub accountView %s %s %s", getStrH(Rd->database, "UserAccounts.PrincipalID"), getStrH(Rd->database, "UserAccounts.FirstName"), getStrH(Rd->database, "UserAccounts.LastName")); + if (1 != c) + { + bitch(Rd, "Cannot edit account.", "Account doesn't exist."); + ret++; + } + else + { +// check if logged in user is allowed to make these changes +// update user record + } + return ret; +} +static int accountExplore(reqData *Rd, inputForm *iF, inputValue *iV) +{ + int ret = 0; +// get a list of user records + return ret; +} +static int accountOut(reqData *Rd, inputForm *iF, inputValue *iV) +{ + int ret = 0; + int c = accountRead(Rd, iF, iV); + + if (1 != c) + { +// bitch(Rd, "Cannot logout account.", "Account doesn't exist."); +// ret++; + } + else { - if (rows) - { - dbPull(Rd, "UserAccounts", rows); - Rd->stuff->putstr(Rd->stuff, "level", getStrH(Rd->database, "UserAccounts.Userlevel")); - free(rows->rows); - free(rows->fieldNames); - free(rows); - } - else - { - Rd->database->putstr(Rd->database, "UserAccounts.PrincipalID", xstrdup(uuid)); - } - Rd->stuff->putstr(Rd->stuff, "UUID", uuid); +// log the user out if they are logged in + Rd->shs.UUID = NULL; + Rd->output = "accountLogin"; } + freeSesh(Rd, FALSE, TRUE); + newSesh(Rd, FALSE); return ret; } -void loginPage(reqData *Rd, char *message) +qhashtbl_t *accountPages = NULL; +inputForm *newInputForm(char *name, char *title, char *help, inputFormShowFunc web, inputFormShowFunc eWeb) { - char *name = xstrdup(getStrH(Rd->stuff, "name")), *linky = checkLinky(Rd); + inputForm *ret = xmalloc(sizeof(inputForm)); + +d("newInputForm(%s)", name); + ret->name = name; ret->title = title; ret->help = help; + ret->web = web; ret->eWeb = eWeb; + ret->fields = qlisttbl(QLISTTBL_THREADSAFE | QLISTTBL_UNIQUE | QLISTTBL_LOOKUPFORWARD); + ret->subs = qhashtbl(0, 0); + accountPages->put(accountPages, ret->name, ret, sizeof(inputForm)); + free(ret); + return accountPages->get(accountPages, name, NULL, false); +} - Rd->stuff->remove(Rd->stuff, "UUID"); - HTMLheader(Rd->reply, " account manager"); - if (DEBUG) HTMLdebug(Rd->reply); - Rd->reply->addstrf(Rd->reply, "

account manager

\n"); - Rd->reply->addstr(Rd->reply, linky); - free(linky); - if (0 != Rd->errors->size(Rd->messages)) - HTMLlist(Rd->reply, "messages -", Rd->messages); - HTMLform(Rd->reply, "", Rd->shs.munchie); - HTMLtext(Rd->reply, "text", "name", "name", name, 42, 63, TRUE); - HTMLtext(Rd->reply, "password", "password", "password", "", 16, 0, TRUE); - Rd->reply->addstr(Rd->reply, "

Warning, the limit on password length is set by your viewer, some can't handle longer than 16 characters.

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

While viewers will usually remember your name and password for you, you'll need to remember it for this web site to.   " - "I highly recommend using a password manager.   KeePass and it's variations is a great password manager.

\n"); - Rd->reply->addstrf(Rd->reply, "\n"); // Stop Enter key on text fields triggering the first submit button. - HTMLbutton(Rd->reply, "login"); - HTMLbutton(Rd->reply, "create"); - if (0 != Rd->errors->size(Rd->errors)) - HTMLlist(Rd->reply, "errors -", Rd->errors); - Rd->reply->addstrf(Rd->reply, "

%s

\n", message); - HTMLfooter(Rd->reply); - free(name); +inputField *addInputField(inputForm *iF, signed char type, char *name, char *title, char *help, inputFieldValidFunc validate, inputFieldShowFunc web) +{ + inputField *ret = xzalloc(sizeof(inputField)); + +//d("addInputField(%s, %s)", iF->name, name); + ret->name = name; ret->title = title; ret->help = help; + ret->validate = validate; ret->web = web; ret->type = type; + ret->flags = FLD_EDITABLE; + iF->fields->put(iF->fields, ret->name, ret, sizeof(inputField)); + free(ret); + return iF->fields->get(iF->fields, name, NULL, false); } -void accountCreationPage(reqData *Rd, char *message) +void inputFieldExtra(inputField *ret, signed char flags, short viewLength, short maxLength) { - char *name = getStrH(Rd->body, "name"), *linky = checkLinky(Rd); - char *toke_n_munchie = getCookie(Rd->Rcookies, "toke_n_munchie"); - char *tmp = xmalloc(16), *t; - int i, d; + ret->flags = flags; + ret->viewLength = viewLength; ret->maxLength = maxLength; +} - if ('\0' == name[0]) - name = getStrH(Rd->stuff, "name"); +void addSession(inputForm *iF) +{ + inputField *fld, **flds = xzalloc(3 * sizeof(*flds)); + +//d("addSession(%s)", iF->name); + flds[0] = addInputField(iF, LUA_TSTRING, "hashish", "hashish", "", sessionValidate, sessionWeb); + inputFieldExtra(flds[0], FLD_HIDDEN, 0, 0); + flds[1] = addInputField(iF, LUA_TSTRING, "toke_n_munchie", "toke_n_munchie", "", sessionValidate, sessionWeb); + inputFieldExtra(flds[1], FLD_HIDDEN, 0, 0); + fld = addInputField(iF, LUA_TGROUP, "sessionGroup", "sessionGroup", "", sessionValidate, sessionWeb); + inputFieldExtra(fld, FLD_HIDDEN, 0, 0); + fld->group = flds; + flds[0]->group = flds; + flds[1]->group = flds; +} -// TODO - eww lots of memory leaks here. -// TODO - need to check if qLibc does it's own free() calls, and fill in the gaps for when it doesn't. - HTMLheader(Rd->reply, " account manager"); - if (DEBUG) HTMLdebug(Rd->reply); - Rd->reply->addstrf(Rd->reply, "

account manager

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

Creating account for %s

\n", name); - Rd->reply->addstr(Rd->reply, linky); - free(linky); - if (0 != Rd->errors->size(Rd->messages)) - HTMLlist(Rd->reply, "messages -", Rd->messages); -// TODO - set this to autocomplete="off". -// TODO - autofill most fields on error and redisplay. - HTMLform(Rd->reply, "", Rd->shs.munchie); - HTMLhidden(Rd->reply, "name", name); - HTMLhidden(Rd->reply, "UUID", getStrH(Rd->stuff, "UUID")); - HTMLtext(Rd->reply, "password", "Re-enter your password", "password", "", 16, 0, FALSE); - Rd->reply->addstr(Rd->reply, "

Warning, the limit on password length is set by your viewer, some can't handle longer than 16 characters.

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

While viewers will usually remember your name and password for you, you'll need to remember it for this web site to.   " - "I highly recommend using a password manager.   KeePass and it's variations is a great password manager.

\n"); - HTMLtext(Rd->reply, "email", "email", "email", getStrH(Rd->body, "email"), 42, 254, FALSE); - HTMLtext(Rd->reply, "email", "Repeat your email, to be sure you got it correct", "emayl", getStrH(Rd->body, "emayl"), 42, 254, FALSE); - Rd->reply->addstr(Rd->reply, "

A validation email will be sent to this email address, you will need to click on the link in it to continue your account creation.

\n"); - Rd->reply->addstr(Rd->reply, "
\n"); - HTMLselect(Rd->reply, "Date of birth", "year"); - t = getStrH(Rd->body, "year"); - if (NULL == t) - d = -1; - else - d = atoi(t); - HTMLoption(Rd->reply, xstrdup(""), FALSE); - for (i = 1900; i <= 2020; i++) - { - boolean sel = FALSE; +void addEmailFields(inputForm *iF) +{ + inputField *fld, **flds = xzalloc(3 * sizeof(*flds)); + + flds[0] = addInputField(iF, LUA_TEMAIL, "email", "email", NULL, emailValidate, emailWeb); + inputFieldExtra(flds[0], FLD_EDITABLE, 42, 254); + flds[1] = addInputField(iF, LUA_TEMAIL, "emayl", "Re-enter your email, to be sure you got it correct", + "A validation email will be sent to this email address, you will need to click on the link in it to continue your account creation.", emailValidate, emailWeb); + inputFieldExtra(flds[1], FLD_EDITABLE, 42, 254); + fld = addInputField(iF, LUA_TGROUP, "emailGroup", "emailGroup", "", emailValidate, NULL); + inputFieldExtra(fld, FLD_HIDDEN, 0, 0); + fld->group = flds; + flds[0]->group = flds; + flds[1]->group = flds; +} - if (i == d) - sel = TRUE; - sprintf(tmp, "%d", i); - HTMLoption(Rd->reply, xstrdup(tmp), sel); - } - HTMLselectEnd(Rd->reply); - Rd->reply->addstr(Rd->reply, ""); - HTMLselect(Rd->reply, NULL, "month"); - t = getStrH(Rd->body, "month"); - HTMLoption(Rd->reply, xstrdup(""), FALSE); - for (i = 0; i <= 11; i++) - { - boolean sel = FALSE; +void addDoBFields(inputForm *iF) +{ + inputField *fld, **flds = xzalloc(3 * sizeof(*flds)); + + flds[0] = addInputField(iF, LUA_TSTRING, "DoByear", "year", NULL, DoBValidate, DoByWeb); + flds[1] = addInputField(iF, LUA_TSTRING, "DoBmonth", "month", NULL, DoBValidate, DoBmWeb); + fld = addInputField(iF, LUA_TGROUP, "DoBGroup", "DoBGroup", "", DoBValidate, DoBWeb); + inputFieldExtra(fld, FLD_HIDDEN, 0, 0); + fld->group = flds; + flds[0]->group = flds; + flds[1]->group = flds; +} - if ((NULL != t) && (strcmp(t, months[i]) == 0)) - sel = TRUE; - HTMLoption(Rd->reply, months[i], sel); - } - HTMLselectEnd(Rd->reply); - Rd->reply->addstr(Rd->reply, "
\n"); - HTMLcheckBox(Rd->reply, "adult", "I'm allegedly an adult in my country.", !strcmp("on", getStrH(Rd->body, "adult")), TRUE); - HTMLcheckBox(Rd->reply, "agree", "I accept the Terms of Service.", !strcmp("on", getStrH(Rd->body, "agree")), TRUE); - Rd->reply->addstrf(Rd->reply, "

Terms of Service

%s
\n", getStrH(Rd->configs, "ToS")); - // For maxlength - the MySQL database field is type text, which has a max length of 64 Kilobytes byets, but characters might take up 1 - 4 bytes, and maxlength is in characters. - // For rows and cols, seems a bit broken, I ask for 5/42, I get 6,36. In world it seems to be 7,46 -// TODO - check against the limit for in world profiles, coz this will become that. -// TODO - validate aboutMe, it should not be empty, and should not be longer than 64 kilobytes. - HTMLtextArea(Rd->reply, "aboutMe", "About me", 7, 50, 4, 16384, "Describe yourself here.", "off", "true", "soft", getStrH(Rd->body, "aboutMe"), FALSE); -// TODO - upload an icon / profile picture. - Rd->reply->addstrf(Rd->reply, "\n"); // Stop Enter key on text fields triggering the first submit button. - HTMLbutton(Rd->reply, "confirm"); - HTMLbutton(Rd->reply, "cancel"); - if (0 != Rd->errors->size(Rd->errors)) - HTMLlist(Rd->reply, "errors -", Rd->errors); - Rd->reply->addstrf(Rd->reply, "

%s

\n", message); - HTMLfooter(Rd->reply); +void addLegalFields(inputForm *iF) +{ + inputField *fld, **flds = xzalloc(3 * sizeof(*flds)); + + flds[0] = addInputField(iF, LUA_TBOOLEAN, "adult", "I'm allegedly an adult in my country.", NULL, legalValidate, adultWeb); + flds[1] = addInputField(iF, LUA_TBOOLEAN, "agree", "I accept the Terms of Service.", NULL, legalValidate, agreeWeb); + fld = addInputField(iF, LUA_TGROUP, "legalGroup", "legalGroup", "", legalValidate, legalWeb); + inputFieldExtra(fld, FLD_HIDDEN, 0, 0); + fld->group = flds; + flds[0]->group = flds; + flds[1]->group = flds; } -void loggedOnPage(reqData *Rd, char *message) +inputSub *addSubmit(inputForm *iF, char *name, char *title, char *help, inputSubmitFunc submit, char *output) { - char *name = getStrH(Rd->stuff, "name"), *linky = checkLinky(Rd); - char *toke_n_munchie = getCookie(Rd->Rcookies, "toke_n_munchie"); + inputSub *ret = xmalloc(sizeof(inputSub)); - HTMLheader(Rd->reply, " account manager"); - if (DEBUG) HTMLdebug(Rd->reply); - Rd->reply->addstrf(Rd->reply, "

account manager

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

account for %s

\n", name); - Rd->reply->addstr(Rd->reply, linky); - free(linky); - if (0 != Rd->errors->size(Rd->messages)) - HTMLlist(Rd->reply, "messages -", Rd->messages); - HTMLform(Rd->reply, "", Rd->shs.munchie); - HTMLhidden(Rd->reply, "name", name); - HTMLhidden(Rd->reply, "UUID", getStrH(Rd->stuff, "UUID")); - HTMLtext(Rd->reply, "email", "email", "email", getStrH(Rd->database, "UserAccounts.Email"), 42, 254, FALSE); - HTMLtext(Rd->reply, "Old password", "password", "password", "", 16, 0, FALSE); - Rd->reply->addstr(Rd->reply, "

Warning, the limit on password length is set by your viewer, some can't handle longer than 16 characters.

\n"); -// HTMLtext(Rd->reply, "title", "text", "title", getStrH(Rh->stuff, "title"), 16, 64, TRUE); - HTMLselect(Rd->reply, "type", "type"); - HTMLoption(Rd->reply, "", false); - HTMLoption(Rd->reply, "newbie", true); - HTMLoption(Rd->reply, "validated", true); - HTMLoption(Rd->reply, "vouched for", true); - HTMLoption(Rd->reply, "approved", true); - HTMLoption(Rd->reply, "disabled", false); - HTMLoption(Rd->reply, "god", false); - HTMLselectEnd(Rd->reply); - Rd->reply->addstrf(Rd->reply, "\n"); // Stop Enter key on text fields triggering the first submit button. - HTMLbutton(Rd->reply, "delete"); - HTMLbutton(Rd->reply, "list"); - HTMLbutton(Rd->reply, "logout"); - HTMLbutton(Rd->reply, "update"); - if (0 != Rd->errors->size(Rd->errors)) - HTMLlist(Rd->reply, "errors -", Rd->errors); - HTMLfooter(Rd->reply); +//d("addSubmit(%s, %s)", iF->name, name); + ret->name = name; ret->title = title; ret->help = help; ret->submit = submit; ret->outputForm = output; + iF->subs->put(iF->subs, ret->name, ret, sizeof(inputSub)); + free(ret); + return iF->subs->get(iF->subs, name, NULL, false); +} + + +/* There should be some precedence for values overriding values here. + https://www.w3.org/standards/webarch/protocols + "This intro text is boilerplate for the beta release of w3.org." Fucking useless. Pffft + https://www.w3.org/Protocols/ + Still nothing official, though the ENV / HEADER stuff tends to be about the protocol things, and cookies / body / queries are about the data things. + http://docs.gantry.org/gantry4/advanced/setby + Says that query overrides cookies, but that might be just for their platform. + https://framework.zend.com/manual/1.11/en/zend.controller.request.html + Says - "1. GET, 2. POST, 3. COOKIE, 4. SERVER, 5. ENV." + We don't actually get the headers directly, it's all sent via the env. + +URL query Values actually provided by the user in the FORM, and other things. +POST body Values actually provided by the user in the FORM. +cookies + https://stackoverflow.com/questions/4056306/how-to-handle-multiple-cookies-with-the-same-name + +headers includes HTTP_COOKIE and QUERY_STRING +env includes headers and HTTP_COOKIE and QUERY_STRING + +database Since all of the above are for updating the database anyway, this goes on the bottom, overridden by all. + Though be wary of security stuff. + + Sending cookie headers is a special case, multiples can be sent, otherwise headers are singletons, only send one for each name. +*/ +char *sourceTypes[] = +{ + "cookies", + "body", + "queries", + "stuff" +}; + +static int collectFields(reqData *Rd, inputForm *iF, inputValue *iV, int t) +{ + int i = 0, j; + qlisttbl_obj_t obj; + + memset((void*)&obj, 0, sizeof(obj)); // must be cleared before call + iF->fields->lock(iF->fields); + while(iF->fields->getnext(iF->fields, &obj, NULL, false) == true) + { + inputField *fld = (inputField *) obj.data; + +//if (0 > t) +// d("Collecting %d %s - %s", t, iF->name, fld->name); +//else +// d("Collecting %s %s - %s", sourceTypes[t], iF->name, fld->name); + iV[i].field = fld; + if (LUA_TGROUP == fld->type) + { + if (0 >= t) + { + j = 0; + // If it's a group, number the members relative to the group field. + // Assume the members for this group are the previous ones. + while (iV[i].field->group[j]) + { + j++; + iV[i - j].index = j; + } + } + } + else + { + char *vl = NULL; + + switch (t) + { + // We don't get the cookies metadata. + case 0 : vl = Rd->cookies->getstr(Rd->cookies, obj.name, false); break; + case 1 : vl = Rd->body-> getstr(Rd->body, obj.name, false); break; + case 2 : vl = Rd->queries->getstr(Rd->queries, obj.name, false); break; + case 3 : vl = Rd->queries->getstr(Rd->stuff, obj.name, false); break; + // Rd->stuff comes from the database reads and calculated stuff, which we do later with this new architecture. The old version validated Rd->stuff as well. + // It was doing that so we can pick up any UUID, validate it, and read in relevant records. + // For newbies their UUID was generated during the validation of name. + // Should still validate that it's long enough and in the correct format, coz that's coming from RD->body sometimes. + // The rest can happen in the submit functions now. + default: break; + } + if ((NULL != iV[i].value) && (NULL != vl)) + { + if (strcmp(vl, iV[i].value) != 0) + W("Collected %s value for %s - %s from %s overriding value from %s", sourceTypes[t], iF->name, fld->name, sourceTypes[t], sourceTypes[iV[i].source]); + else + W("Collected %s value for %s - %s from %s same as value from %s", sourceTypes[t], iF->name, fld->name, sourceTypes[t], sourceTypes[iV[i].source]); + } + if (NULL != vl) + { + iV[i].source = t; + iV[i].value = vl; + D("Collected %s value for %s - %s = %s", sourceTypes[t], iF->name, fld->name, vl); + } + } + i++; + } + iF->fields->unlock(iF->fields); + return i; } + void listPage(reqData *Rd, char *message) { // TODO - should check if the user is a god before allowing this. @@ -4267,8 +4927,9 @@ void listPage(reqData *Rd, char *message) NULL, NULL, "Name"), "member accounts", NULL, NULL); HTMLform(Rd->reply, "", Rd->shs.munchie); + HTMLhidden(Rd->reply, "form", "accountExplore"); HTMLhidden(Rd->reply, "name", name); - HTMLhidden(Rd->reply, "UUID", getStrH(Rd->stuff, "UUID")); + HTMLhidden(Rd->reply, "UUID", Rd->shs.UUID); Rd->reply->addstrf(Rd->reply, "\n"); // Stop Enter key on text fields triggering the first submit button. HTMLbutton(Rd->reply, "me"); HTMLbutton(Rd->reply, "logout"); @@ -4278,91 +4939,25 @@ void listPage(reqData *Rd, char *message) HTMLfooter(Rd->reply); } + void account_html(char *file, reqData *Rd, HTMLfile *thisFile) { + inputForm *iF; + inputField *fld; + inputSub *sub; boolean isGET = FALSE; - int e = 0; - char *doit = getStrH(Rd->body, "doit"); - - C("Starting dynamic page %s", file); - Rd->func = NULL; - - if (NULL == fieldValidFuncs) - { - fieldValidFuncs = qlisttbl(QLISTTBL_LOOKUPFORWARD | QLISTTBL_THREADSAFE | QLISTTBL_UNIQUE); - newValidFunc("hashish", "session", (fieldValidFunc) validateSesh); - newValidFunc("toke_n_munchie", "session", (fieldValidFunc) validateSesh); - newValidFunc("UUID", "UUID", (fieldValidFunc) validateUUID); - newValidFunc("name", "name", (fieldValidFunc) validateName); - newValidFunc("password", "password", (fieldValidFunc) validatePassword); - newValidFunc("email", "email", (fieldValidFunc) validateEmail); - newValidFunc("emayl", "email", (fieldValidFunc) validateEmail); - newValidFunc("year", "DoB", (fieldValidFunc) validateDoB); - newValidFunc("month", "DoB", (fieldValidFunc) validateDoB); - newValidFunc("adult", "legal", (fieldValidFunc) validateLegal); - newValidFunc("agree", "legal", (fieldValidFunc) validateLegal); - newValidFunc("aboutMe", "about me", (fieldValidFunc) validateAboutMe); - } - if (NULL == buildPages) - { - buildPages = qhashtbl(0, 0); - newBuildPage("login", (pageBuildFunction) loggedOnPage, (pageBuildFunction) loginPage); - newBuildPage("cancel", (pageBuildFunction) loginPage, (pageBuildFunction) loginPage); - newBuildPage("logout", (pageBuildFunction) loginPage, (pageBuildFunction) loginPage); - newBuildPage("create", (pageBuildFunction) accountCreationPage, (pageBuildFunction) loginPage); - newBuildPage("confirm", (pageBuildFunction) loggedOnPage, (pageBuildFunction) accountCreationPage); - newBuildPage("me", (pageBuildFunction) loggedOnPage, (pageBuildFunction) loginPage); - newBuildPage("update", (pageBuildFunction) loggedOnPage, (pageBuildFunction) loginPage); - newBuildPage("delete", (pageBuildFunction) loginPage, (pageBuildFunction) loginPage); - newBuildPage("list", (pageBuildFunction) listPage, (pageBuildFunction) loginPage); - } - - if ('\0' == doit[0]) - doit = getStrH(Rd->cookies, "doit"); - if ('\0' == doit[0]) - doit = "logout"; - if ('\0' != doit[0]) - { - setCookie(Rd, "doit", doit); - Rd->doit = doit; - } - - e += validateThings(Rd, doit, "cookies", Rd->cookies); - e += validateThings(Rd, doit, "body", Rd->body); - e += validateThings(Rd, doit, "queries", Rd->queries); - e += validateThings(Rd, doit, "session", Rd->stuff); - - if (NULL == Rd->func) - { - buildPage *bp = buildPages->get(buildPages, doit, NULL, false); - if (bp) - { - if (e) - { - Rd->func = bp->eFunc; - E("Got page builder ERROR function for %s, coz of %d errors.", doit, e); - } - else - { - Rd->func = bp->func; - D("Got page builder function for %s.", doit); - } - } - } - if (NULL == Rd->func) - Rd->func = (pageBuildFunction) loginPage; + int e = 0, t = 0, i, j; + char *doit = getStrH(Rd->body, "doit"), *form = getStrH(Rd->body, "form"); - if (strcmp("https", Rd->Scheme) != 0) + if (NULL == accountLevels) { - 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; + accountLevels = qlisttbl(QLISTTBL_LOOKUPFORWARD | QLISTTBL_THREADSAFE | QLISTTBL_UNIQUE); + accountLevels->putstr(accountLevels, "-256", "disabled"); + accountLevels->putstr(accountLevels, "-200", "newbie"); + accountLevels->putstr(accountLevels, "-100", "validated"); + accountLevels->putstr(accountLevels, "-50", "vouched for"); + accountLevels->putstr(accountLevels, "1", "approved"); + accountLevels->putstr(accountLevels, "200", "god"); } // Check "Origin" header and /or HTTP_REFERER header. @@ -4380,7 +4975,7 @@ void account_html(char *file, reqData *Rd, HTMLfile *thisFile) { bitch(Rd, "Invalid referer.", ref); D("Invalid referer - %s isn't %s", ref, href); - Rd->func = (pageBuildFunction) loginPage; + form = "accountLogin"; } free(href); } @@ -4393,66 +4988,260 @@ void account_html(char *file, reqData *Rd, HTMLfile *thisFile) { bitch(Rd, "Invalid HOST.", ref); D("Invalid HOST - %s isn't %s", ref, href); - Rd->func = (pageBuildFunction) loginPage; + form = "accountLogin"; + } + + // Redirect to HTTPS if it's HTTP. + if (strcmp("https", Rd->Scheme) != 0) + { + Rd->Rheaders->putstr (Rd->Rheaders, "Status", "301 Moved Permanently"); + Rd->Rheaders->putstrf(Rd->Rheaders, "Location", "https://%s%s", Rd->Host, Rd->RUri); + Rd->reply->addstrf(Rd->reply, "404 Unknown page" + "" + "You should get redirected to https://%s%s", + Rd->Host, Rd->RUri, Rd->Host, Rd->RUri, Rd->Host, Rd->RUri + ); + D("Redirecting dynamic page %s -> https://%s%s", file, Rd->Host, Rd->RUri); + return; + } + + // Create the dynamic web pages for account.html. + if (NULL == accountPages) + { + accountPages = qhashtbl(0, 0); + + + iF = newInputForm("accountAdd", "", NULL, accountAddWeb, accountLoginWeb); + addSession(iF); +// fld = addInputField(iF, LUA_TSTRING, "UUID", "UUID", NULL, UUIDValidate, UUIDWeb); +// inputFieldExtra(fld, FLD_HIDDEN, 0, 0); + fld = addInputField(iF, LUA_TSTRING, "name", "name", NULL, nameValidate, nameWeb); + inputFieldExtra(fld, FLD_EDITABLE | FLD_REQUIRED, 42, 63); + fld = addInputField(iF, LUA_TPASSWORD, "psswrd", "Re-enter your password", + "Warning, the limit on password length is set by your viewer, some can't handle longer than 16 characters.", passwordValidate, passwordWeb); + inputFieldExtra(fld, FLD_EDITABLE, 16, 0); + addEmailFields(iF); + addDoBFields(iF); + addLegalFields(iF); + fld = addInputField(iF, LUA_TSTRING, "ToS", "Terms of Service", "", NULL, ToSWeb); + fld = addInputField(iF, LUA_TSTRING, "aboutMe", "About me", NULL, aboutMeValidate, aboutMeWeb); + inputFieldExtra(fld, FLD_EDITABLE, 50, 16384); + addSubmit(iF, "confirm", "confirm", NULL, accountAdd, "accountView"); + addSubmit(iF, "cancel", "cancel", NULL, accountOut, "accountLogin"); + + + iF = newInputForm("accountView", "account view", NULL, accountViewWeb, accountLoginWeb); + addSession(iF); +// fld = addInputField(iF, LUA_TSTRING, "UUID", "UUID", NULL, UUIDValidate, UUIDWeb); +// inputFieldExtra(fld, FLD_HIDDEN, 0, 0); + fld = addInputField(iF, LUA_TSTRING, "name", "name", NULL, nameValidate, nameWeb); + inputFieldExtra(fld, FLD_HIDDEN, 42, 63); + addSubmit(iF, "login", "", NULL, accountView, "accountView"); // Coz we sometimes want to trigger this from code. + addSubmit(iF, "validate", "", NULL, accountValidate, "accountLogin"); // Coz we sometimes want to trigger this from code. +// addSubmit(iF, "edit", "edit", NULL, accountEdit, "accountEdit"); +// addSubmit(iF, "members", "members", NULL, accountExplore, "accountExplore"); + addSubmit(iF, "logout", "logout", NULL, accountOut, "accountLogin"); + + + iF = newInputForm("accountEdit", "account edit", NULL, accountEditWeb, accountLoginWeb); + addSession(iF); +// fld = addInputField(iF, LUA_TSTRING, "UUID", "UUID", NULL, UUIDValidate, UUIDWeb); +// inputFieldExtra(fld, FLD_HIDDEN, 0, 0); + fld = addInputField(iF, LUA_TSTRING, "name", "name", NULL, nameValidate, nameWeb); + inputFieldExtra(fld, FLD_HIDDEN, 42, 63); + fld = addInputField(iF, LUA_TEMAIL, "email", "email", "", emailValidate, emailWeb); + inputFieldExtra(fld, FLD_NONE, 42, 254); + addSubmit(iF, "login", "", NULL, accountView, "accountView"); // Coz we sometimes want to trigger this from code. + addSubmit(iF, "save", "save", NULL, accountSave, "accountSave"); + addSubmit(iF, "cancel", "cancel", NULL, accountOut, "accountView"); +// addSubmit(iF, "members", "members", NULL, accountExplore, "accountExplore"); + addSubmit(iF, "logout", "logout", NULL, accountOut, "accountLogin"); +// addSubmit(iF, "delete", "delete", NULL, accountDel, "accountDel"); + + + iF = newInputForm("accountLogin", "account login", "Please login, or create your new account.", accountLoginWeb, accountLoginWeb); + addSession(iF); + fld = addInputField(iF, LUA_TSTRING, "name", "name", "Your name needs to be two words, only letters and numbers.", nameValidate, nameWeb); + inputFieldExtra(fld, FLD_EDITABLE | FLD_REQUIRED, 42, 63); + fld = addInputField(iF, LUA_TPASSWORD, "password", "password", + "Warning, the limit on password length is set by your viewer, some can't handle longer than 16 characters.", passwordValidate, passwordWeb); + inputFieldExtra(fld, FLD_EDITABLE | FLD_REQUIRED, 16, 0); + addSubmit(iF, "logout", "", NULL, accountOut, "accountLogin"); // Coz we sometimes want to trigger this from code. + addSubmit(iF, "validate", "", NULL, accountValidate, "accountLogin"); // Coz we sometimes want to trigger this from code. + addSubmit(iF, "login", "login", NULL, accountView, "accountView"); + addSubmit(iF, "create", "create", NULL, accountCreate, "accountAdd"); + } + + // Figure out what we are doing. + if ('\0' == form[0]) + form = getStrH(Rd->cookies, "form"); + if ('\0' == form[0]) + form = "accountLogin"; + 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; } - // Redirect to a GET if it was a POST. - if ((0 == e) && (strcmp("POST", Rd->Method) == 0)) + iF = accountPages->get(accountPages, form, NULL, false); + if (NULL == iF) + { + E("No such account page - %s", form); + form = "accountLogin"; + doit = "logout"; + iF = accountPages->get(accountPages, form, NULL, false); + } + sub = iF->subs->get(iF->subs, doit, NULL, false); + if (NULL == sub) + { + E("No such account action - %s", doit); + form = "accountLogin"; + doit = "logout"; + iF = accountPages->get(accountPages, form, NULL, false); + sub = iF->subs->get(iF->subs, doit, NULL, false); + } + + Rd->doit = doit; + Rd->form = form; + Rd->output = sub->outputForm; + + C("Starting dynamic page %s %s -> %s [%s -> %s]", Rd->RUri, form, doit, sub->name, Rd->output); + + // Collect the input data. + int count = iF->fields->size(iF->fields); + inputValue *iV = xzalloc(count * sizeof(inputValue)); + qlisttbl_obj_t obj; + + if (strcmp("cancel", sub->name) != 0) { - if (Rd->func == (pageBuildFunction) loginPage) - freeSesh(Rd, FALSE, TRUE); - if (strcmp("confirm", doit) == 0) +// TODO - complain about any extra body or query parameter that we where not expecting. + for (t = 0; t < 3; t++) + i = collectFields(Rd, iF, iV, t); + + // Validate the input data. + D("For page %s -> %s, start of validation.", iF->name, sub->name); + t = i; + for (i = 0; i < t; i++) + { + if ((NULL != iV[i].value) || (LUA_TGROUP == iV[i].field->type)) { - createUser(Rd); - newSesh(Rd, TRUE); - Rd->chillOut = TRUE; + if (NULL == iV[i].field->validate) + E("No validation function for %s - %s", iF->name, iV[i].field->name); + else + { + if (0 == iV[i].valid) + { + D("Validating %s - %s", iF->name, iV[i].field->name); + int rt = iV[i].field->validate(Rd, iF, &iV[i]); + if (rt) + { + W("Invalidated %s - %s returned %d errors", iF->name, iV[i].field->name, rt); + iV[i].valid = -1; + } + else + { + D("Validated %s - %s", iF->name, iV[i].field->name); + iV[i].valid = 1; + } + e += rt; + if (NULL != iV[i].field->group) + { + // Use the indexes to set the validation result for the other members of the group. + // The assumption is that the validation functions for each are the same, and it validates the members as a group. + for (j = iV[i].index; j > 0; j--) + iV[i + j].valid = iV[i].valid; +// TODO - Possible off by one error, but I don't think it matters. Needs more testing. + for (j = 0; j <= iV[i].index; j++) + iV[i + j].valid = iV[i].valid; + } + } + else if (0 > iV[i].valid) + D("Already invalidated %s - %s", iF->name, iV[i].field->name); + else if (0 < iV[i].valid) + D("Already validated %s - %s", iF->name, iV[i].field->name); + } } + doit = Rd->doit; + } + + // Submit the data. + sub = iF->subs->get(iF->subs, Rd->doit, NULL, false); +//if (strcmp("POST", Rd->Method) == 0) // Not such a good idea, since we are faking a submit for account validation, which is a GET. +{ + if (0 == e) + { + D("For page %s, start of %s submission.", iF->name, sub->name); + if (NULL != sub->submit) + e += sub->submit(Rd, iF, iV); + } +} + free(iV); - if (strcmp("login", doit) == 0) - Rd->chillOut = TRUE; + } - if (Rd->vegOut) + // Return the result. + if (0 == e) + { + if (strcmp("GET", Rd->Method) == 0) { - t("vegOut"); - freeSesh(Rd, FALSE, TRUE); + // Find the output form. + inputForm *oF = accountPages->get(accountPages, Rd->output, NULL, false); + if (NULL == oF) + { + E("No such account page - %s", Rd->output); + form = "accountLogin"; + doit = "logout"; + oF = accountPages->get(accountPages, form, NULL, false); + } + D("Building output page %s", oF->name); + count = oF->fields->size(oF->fields); + iV = xzalloc(count * sizeof(inputValue)); + collectFields(Rd, oF, iV, -1); + collectFields(Rd, oF, iV, 3); + oF->web(Rd, oF, iV); + free(iV); } - else if (Rd->chillOut) + else { - 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" + // Redirect to a GET if it was a POST. + if ((strcmp("POST", Rd->Method) == 0)) + { + if ('\0' != Rd->form[0]) + setCookie(Rd, "form", Rd->form); + if ('\0' != Rd->doit[0]) + setCookie(Rd, "doit", Rd->doit); + Rd->Rheaders->putstr (Rd->Rheaders, "Status", "303 See Other"); + Rd->Rheaders->putstrf(Rd->Rheaders, "Location", "https://%s%s", 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); + I("Redirecting dynamic page %s -> https://%s%s (%s)", file, Rd->Host, Rd->RUri, Rd->form); + } + } } - else // Actually send the page. + else { - if (Rd->func == (pageBuildFunction) loginPage) - { - if (strcmp("confirm", doit) != 0) - freeSesh(Rd, FALSE, TRUE); - newSesh(Rd, FALSE); - } - else if ((0 != e)) // So we can reload details into a broken form, so the user doesn't have to retype everything. - { - freeSesh(Rd, FALSE, FALSE); - newSesh(Rd, FALSE); - } - - Rd->func(Rd, ""); + if (0 < e) + E("Building output ERROR page %s, coz of %d errors.", iF->name, e); + else + D("Building alternate output page %s", iF->name); + // First just sort out groups, then get the data from Rd->stuff. + count = iF->fields->size(iF->fields); + iV = xzalloc(count * sizeof(inputValue)); + collectFields(Rd, iF, iV, -1); + collectFields(Rd, iF, iV, 3); + iF->eWeb(Rd, iF, iV); + free(iV); } - C("Ending dynamic page %s", file); + C("Ending dynamic page %s %s", Rd->RUri, form); } @@ -4470,9 +5259,24 @@ static void cleanup(void) dbFreeRequest(req); dbRequests->removefirst(dbRequests); } - if (fieldValidFuncs) fieldValidFuncs->free(fieldValidFuncs); + + if (accountPages) + { + qhashtbl_obj_t obj; + + memset((void*)&obj, 0, sizeof(obj)); + accountPages->lock(accountPages); + while(accountPages->getnext(accountPages, &obj, true) == true) + { +// d("%s = %s", obj.name, (char *) obj.data); + } + accountPages->unlock(accountPages); + accountPages->free(accountPages); + } + +// if (fieldValidFuncs) fieldValidFuncs->free(fieldValidFuncs); +// if (buildPages) buildPages->free(buildPages); if (dynPages) dynPages->free(dynPages); - if (buildPages) buildPages->free(buildPages); if (HTMLfileCache) HTMLfileCache->free(HTMLfileCache); if (HTMLfileCache) mimeTypes->free(mimeTypes); if (dbRequests) dbRequests->free(dbRequests); @@ -4491,7 +5295,6 @@ void sledjchisl_main(void) { char *cmd = *toys.optargs; char *tmp; - MYSQL *database = NULL, *dbconn = NULL; gridStats *stats = NULL; struct stat statbuf; int status, result, i; @@ -4521,14 +5324,14 @@ void sledjchisl_main(void) } else { - if (S_ISREG (statbuf.st_mode)) d("STDIN is a regular file."); - else if (S_ISDIR (statbuf.st_mode)) d("STDIN is a directory."); - else if (S_ISCHR (statbuf.st_mode)) d("STDIN is a character device."); - else if (S_ISBLK (statbuf.st_mode)) d("STDIN is a block device."); - else if (S_ISFIFO(statbuf.st_mode)) d("STDIN is a FIFO (named pipe)."); - else if (S_ISLNK (statbuf.st_mode)) d("STDIN is a symbolic link."); - else if (S_ISSOCK(statbuf.st_mode)) d("STDIN is a socket."); - else d("STDIN is a unknown file descriptor type."); + if (S_ISREG (statbuf.st_mode)) D("STDIN is a regular file."); + else if (S_ISDIR (statbuf.st_mode)) D("STDIN is a directory."); + else if (S_ISCHR (statbuf.st_mode)) D("STDIN is a character device."); + else if (S_ISBLK (statbuf.st_mode)) D("STDIN is a block device."); + else if (S_ISFIFO(statbuf.st_mode)) D("STDIN is a FIFO (named pipe)."); + else if (S_ISLNK (statbuf.st_mode)) D("STDIN is a symbolic link."); + else if (S_ISSOCK(statbuf.st_mode)) D("STDIN is a socket."); + else D("STDIN is a unknown file descriptor type."); if (!S_ISCHR(statbuf.st_mode)) isWeb = TRUE; } @@ -4543,7 +5346,7 @@ void sledjchisl_main(void) int ngroups; pw = xgetpwuid(euid); - D("Running as user %s", pw->pw_name); + I("Running as user %s", pw->pw_name); grp = xgetgrgid(egid); ngroups = getgroups(i, groups); @@ -4631,20 +5434,20 @@ jit library is loaded or the JIT compiler will not be activated. } } DEBUG = configs->getint(configs, "debug"); - d("Setting DEBUG = %d", DEBUG); - if ((vd = configs->get (configs, "loadAverageInc", NULL, false)) != NULL) {loadAverageInc = *((float *) vd); d("Setting loadAverageInc = %f", loadAverageInc);} - if ((vd = configs->get (configs, "simTimeOut", NULL, false)) != NULL) {simTimeOut = (int) *((float *) vd); d("Setting simTimeOut = %d", simTimeOut);} - if ((tmp = configs->getstr(configs, "scRoot", false)) != NULL) {scRoot = tmp; d("Setting scRoot = %s", scRoot);} - if ((tmp = configs->getstr(configs, "scUser", false)) != NULL) {scUser = tmp; d("Setting scUser = %s", scUser);} - if ((tmp = configs->getstr(configs, "Tconsole", false)) != NULL) {Tconsole = tmp; d("Setting Tconsole = %s", Tconsole);} - if ((tmp = configs->getstr(configs, "Tsocket", false)) != NULL) {Tsocket = tmp; d("Setting Tsocket = %s", Tsocket);} - if ((tmp = configs->getstr(configs, "Ttab", false)) != NULL) {Ttab = tmp; d("Setting Ttab = %s", Ttab);} - if ((tmp = configs->getstr(configs, "webRoot", false)) != NULL) {webRoot = tmp; d("Setting webRoot = %s", webRoot);} - if ((tmp = configs->getstr(configs, "URL", false)) != NULL) {URL = tmp; d("Setting URL = %s", URL);} - if ((vd = configs->get (configs, "seshTimeOut", NULL, false)) != NULL) {seshTimeOut = (int) *((float *) vd); d("Setting seshTimeOut = %d", seshTimeOut);} - if ((vd = configs->get (configs, "idleTimeOut", NULL, false)) != NULL) {idleTimeOut = (int) *((float *) vd); d("Setting idleTimeOut = %d", idleTimeOut);} - if ((vd = configs->get (configs, "newbieTimeOut", NULL, false)) != NULL) {newbieTimeOut = (int) *((float *) vd); d("Setting newbieTimeOut = %d", newbieTimeOut);} - if ((tmp = configs->getstr(configs, "ToS", false)) != NULL) {ToS = tmp; d("Setting ToS = %s", ToS);} + D("Setting DEBUG = %d", DEBUG); + if ((vd = configs->get (configs, "loadAverageInc", NULL, false)) != NULL) {loadAverageInc = *((float *) vd); D("Setting loadAverageInc = %f", loadAverageInc);} + if ((vd = configs->get (configs, "simTimeOut", NULL, false)) != NULL) {simTimeOut = (int) *((float *) vd); D("Setting simTimeOut = %d", simTimeOut);} + if ((tmp = configs->getstr(configs, "scRoot", false)) != NULL) {scRoot = tmp; D("Setting scRoot = %s", scRoot);} + if ((tmp = configs->getstr(configs, "scUser", false)) != NULL) {scUser = tmp; D("Setting scUser = %s", scUser);} + if ((tmp = configs->getstr(configs, "Tconsole", false)) != NULL) {Tconsole = tmp; D("Setting Tconsole = %s", Tconsole);} + if ((tmp = configs->getstr(configs, "Tsocket", false)) != NULL) {Tsocket = tmp; D("Setting Tsocket = %s", Tsocket);} + if ((tmp = configs->getstr(configs, "Ttab", false)) != NULL) {Ttab = tmp; D("Setting Ttab = %s", Ttab);} + if ((tmp = configs->getstr(configs, "webRoot", false)) != NULL) {webRoot = tmp; D("Setting webRoot = %s", webRoot);} + if ((tmp = configs->getstr(configs, "URL", false)) != NULL) {URL = tmp; D("Setting URL = %s", URL);} + if ((vd = configs->get (configs, "seshTimeOut", NULL, false)) != NULL) {seshTimeOut = (int) *((float *) vd); D("Setting seshTimeOut = %d", seshTimeOut);} + if ((vd = configs->get (configs, "idleTimeOut", NULL, false)) != NULL) {idleTimeOut = (int) *((float *) vd); D("Setting idleTimeOut = %d", idleTimeOut);} + if ((vd = configs->get (configs, "newbieTimeOut", NULL, false)) != NULL) {newbieTimeOut = (int) *((float *) vd); D("Setting newbieTimeOut = %d", newbieTimeOut);} + if ((tmp = configs->getstr(configs, "ToS", false)) != NULL) {ToS = tmp; D("Setting ToS = %s", ToS);} // Use a FHS compatible setup - @@ -4824,19 +5627,8 @@ jit library is loaded or the JIT compiler will not be activated. } else { - dbconn = mysql_real_connect(database, - getStrH(configs, "Data Source"), - getStrH(configs, "User ID"), - getStrH(configs, "Password"), - getStrH(configs, "Database"), -// 3036, "/var/run/mysqld/mysqld.sock", - 0, NULL, - CLIENT_FOUND_ROWS | CLIENT_LOCAL_FILES | CLIENT_MULTI_STATEMENTS | CLIENT_MULTI_RESULTS); - if (NULL == dbconn) - { - E("mysql_real_connect() failed - %s", mysql_error(database)); + if (!dbConnect()) goto finished; - } // Need to kick this off. stats = getStats(database, stats); @@ -4867,11 +5659,11 @@ jit library is loaded or the JIT compiler will not be activated. I("Running SledjChisl inside a web server, pid %d.", getpid()); if (0 == toys.optc) - d("no args"); + D("no args"); else { for (entries = 0, bytes = -1; entries < toys.optc; entries++) - d("ARG %s\n", toys.optargs[entries]); + D("ARG %s\n", toys.optargs[entries]); } printEnv(environ); @@ -4967,32 +5759,40 @@ if ((strcmp("HTTP_COOKIE", ky) == 0) || (strcmp("CONTENT_LENGTH", ky) == 0) || ( t("COOKIES"); Rd->cookies = toknize(Rd->headers->getstr(Rd->headers, "HTTP_COOKIE", false), "=;"); santize(Rd->cookies, TRUE); + if (strcmp("GET", Rd->Method) == 0) + { // In theory a POST has body fields INSTEAD of query fields. Especially for ignoring the linky-hashish after the validation page. t("QUERY"); - Rd->queries = toknize(Rd->headers->getstr(Rd->headers, "QUERY_STRING", false), "=&"); - santize(Rd->queries, TRUE); + Rd->queries = toknize(Rd->headers->getstr(Rd->headers, "QUERY_STRING", false), "=&"); + santize(Rd->queries, TRUE); + } + else + { +T("ignoring QUERY"); + Rd->queries = qhashtbl(0, 0); +// TODO - stop this from leaking. + Rd->RUri = xmprintf("%s%s", Rd->Script, Path); + } +t("BODY"); char *Body = NULL; if (Length != NULL) { - size_t len = strtol(Length, NULL, 10); - Body = xmalloc(len + 1); - int c = FCGI_fread(Body, 1, len, FCGI_stdin); - if (c != len) - { - E("Tried to read %d of the body, only got %d!", len, c); - } - Body[len] = '\0'; + size_t len = strtol(Length, NULL, 10); + Body = xmalloc(len + 1); + int c = FCGI_fread(Body, 1, len, FCGI_stdin); + if (c != len) + { + E("Tried to read %d of the body, only got %d!", len, c); + } + Body[len] = '\0'; } else - Length = "0"; -t("BODY"); + Length = "0"; Rd->body = toknize(Body, "=&"); free(Body); santize(Rd->body, TRUE); - - I("Started FCGI web %s request ROLE = %s, body is %s bytes, pid %d.", Rd->Method, Role, Length, getpid()); - D("%s://%s%s -> %s%s", Rd->Scheme, Rd->Host, Rd->RUri, webRoot, Path); - + I("%s %s://%s%s -> %s%s", Rd->Method, Rd->Scheme, Rd->Host, Rd->RUri, webRoot, Path); + D("Started FCGI web request ROLE = %s, body is %s bytes, pid %d.", Role, Length, getpid()); /* TODO - other headers may include - different Content-type @@ -5037,7 +5837,7 @@ t("BODY"); E("Failed to open %s, it's not a virtual file either", toybuf); goto sendReply; } - I("Dynamic page %s found.", dp->name); +// 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? @@ -5164,6 +5964,7 @@ fcgiDone: qhashtbl_free(Rd->cookies); qhashtbl_free(Rd->body); qhashtbl_free(Rd->queries); + if (Rd->lnk) free(Rd->lnk); struct timespec now; if (-1 == clock_gettime(CLOCK_REALTIME, &now)) @@ -5240,8 +6041,8 @@ fcgiDone: free(d); } - for (i = 0; i < sims->num; i++) -// for (i = 0; i < 2; i++) +// for (i = 0; i < sims->num; i++) + for (i = 0; i < 2; i++) { char *sim = sims->sims[i], *name = getSimName(sims->sims[i]); -- cgit v1.1