aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/src/sledjchisl/sledjchisl.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/sledjchisl/sledjchisl.c')
-rw-r--r--src/sledjchisl/sledjchisl.c7342
1 files changed, 7342 insertions, 0 deletions
diff --git a/src/sledjchisl/sledjchisl.c b/src/sledjchisl/sledjchisl.c
new file mode 100644
index 0000000..42ed205
--- /dev/null
+++ b/src/sledjchisl/sledjchisl.c
@@ -0,0 +1,7342 @@
1/* sledjchisl.c - opensim-SC management system.
2 *
3 * Copyright 2020 David Seikel <sledjchisl@sledjhamr.org>
4 * Not in SUSv4. An entirely new invention, thus no web site either.
5
6USE_SLEDJCHISL(NEWTOY(sledjchisl, "m(mode):", TOYFLAG_USR|TOYFLAG_BIN))
7
8config SLEDJCHISL
9 bool "sledjchisl"
10 default y
11 help
12 usage: sledjchisl [-m|--mode mode]
13
14 opensim-SC management system.
15*/
16
17
18// TODO - figure out how to automate testing of this.
19// Being all interactive and involving external web servers / viewers makes it hard.
20
21// TODO - do all the setup on first run, and check if needed on each start up, to be self healing.
22
23// TODO - pepper could be entered on the console on startup if it's not defined, as a master password sort of thing.
24// I'd go as far as protecting the database credentials that way, but legacy OpenSim needs it unprotected.
25// Also keep in mind, people want autostart of their services without having to enter passwords on each boot.
26
27// TODO - once it is event driven, periodically run things like session clean ups, self healing, and the secure.sh thing.
28// And backups off course.
29// As well as regular database pings to keep the connection open.
30
31#include <fcgi_config.h>
32#ifdef _WIN32
33#include <process.h>
34#else
35extern char **environ;
36#endif
37// Don't overide standard stdio stuff.
38#define NO_FCGI_DEFINES
39#include <fcgi_stdio.h>
40#undef NO_FCGI_DEFINES
41//#include "fcgiapp.h"
42
43#include <lua.h>
44#include <lualib.h>
45#include <lauxlib.h>
46#include <luajit.h>
47
48#include "lib/fcgi_SC.h"
49#include "lib/handlekeys.h"
50
51// Both my_config.h and fcgi_config.h define the same PACKAGE* variables, which we don't use anyway,
52// I deal with that by using a sed invokation when building fcgi2.
53
54// https://mariadb.com/kb/en/about-mariadb-connector-c/ Official docs.
55// http://dev.mysql.com/doc/refman/5.5/en/c-api-function-overview.html MySQL docs.
56// http://zetcode.com/db/mysqlc/ MySQL tutorial.
57#include <my_global.h>
58#include <mysql.h>
59
60#include <qlibc.h>
61#include <extensions/qconfig.h>
62
63// TODO - I should probably replace openSSL with something else. Only using it for the hash functions, and apparently it's got a bit of a bad rep.
64// qLibc optionally uses openSSL for it's HTTP client stuff.
65#include <openssl/crypto.h>
66#include <openssl/evp.h>
67#include "openssl/hmac.h"
68#include <uuid/uuid.h>
69
70// Toybox's strend overrides another strend that causes MariaDB library to crash. Renaming it to tb_strend helps.
71// I deal with that by using a sed invokation when building toybox.
72#include "toys.h"
73
74
75GLOBALS(
76 char *mode;
77)
78
79#define TT this.sledjchisl
80
81#define FLAG_m 2
82
83
84
85// Duplicate some small amount of code from qLibc, coz /, + and, = are not good choices, and the standard says we can pick those.
86/**
87 * Encode data using BASE64 algorithm.
88 *
89 * @param bin a pointer of input data.
90 * @param size the length of input data.
91 *
92 * @return a malloced string pointer of BASE64 encoded string in case of
93 * successful, otherwise returns NULL
94 *
95 * @code
96 * const char *text = "hello world";
97 *
98 * char *encstr = qB64_encode(text, strlen(text));
99 * if(encstr == NULL) return -1;
100 *
101 * printf("Original: %s\n", text);
102 * printf("Encoded : %s\n", encstr);
103 *
104 * size_t decsize = qB64_decode(encstr);
105 *
106 * printf("Decoded : %s (%zu bytes)\n", encstr, decsize);
107 * free(encstr);
108 *
109 * --[output]--
110 * Original: hello world
111 * Encoded: aGVsbG8gd29ybGQ=
112 * Decoded: hello world (11 bytes)
113 * @endcode
114 */
115char *qB64_encode(const void *bin, size_t size) {
116 const char B64CHARTBL[64] = {
117 'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P', // 00-0F
118 'Q','R','S','T','U','V','W','X','Y','Z','a','b','c','d','e','f', // 10-1F
119 'g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v', // 20-2F
120 'w','x','y','z','0','1','2','3','4','5','6','7','8','9','+','/' // 30-3F
121 };
122
123 if (size == 0) {
124 return strdup("");
125 }
126
127 // malloc for encoded string
128 char *pszB64 = (char *) malloc(
129 4 * ((size / 3) + ((size % 3 == 0) ? 0 : 1)) + 1);
130 if (pszB64 == NULL) {
131 return NULL;
132 }
133
134 char *pszB64Pt = pszB64;
135 unsigned char *pBinPt, *pBinEnd = (unsigned char *) (bin + size - 1);
136 unsigned char szIn[3] = { 0, 0, 0 };
137 int nOffset;
138 for (pBinPt = (unsigned char *) bin, nOffset = 0; pBinPt <= pBinEnd;
139 pBinPt++, nOffset++) {
140 int nIdxOfThree = nOffset % 3;
141 szIn[nIdxOfThree] = *pBinPt;
142 if (nIdxOfThree < 2 && pBinPt < pBinEnd)
143 continue;
144
145 *pszB64Pt++ = B64CHARTBL[((szIn[0] & 0xFC) >> 2)];
146 *pszB64Pt++ = B64CHARTBL[(((szIn[0] & 0x03) << 4)
147 | ((szIn[1] & 0xF0) >> 4))];
148 *pszB64Pt++ =
149 (nIdxOfThree >= 1) ?
150 B64CHARTBL[(((szIn[1] & 0x0F) << 2)
151 | ((szIn[2] & 0xC0) >> 6))] :
152 '=';
153 *pszB64Pt++ = (nIdxOfThree >= 2) ? B64CHARTBL[(szIn[2] & 0x3F)] : '=';
154
155 memset((void *) szIn, 0, sizeof(szIn));
156 }
157 *pszB64Pt = '\0';
158
159 pszB64 = qstrreplace("tr", pszB64, "+", "~");
160 pszB64 = qstrreplace("tr", pszB64, "/", "_");
161 pszB64 = qstrreplace("tr", pszB64, "=", "^");
162
163 return pszB64;
164}
165
166/**
167 * Decode BASE64 encoded string.
168 *
169 * @param str a pointer of Base64 encoded string.
170 *
171 * @return the length of bytes stored in the str memory in case of successful,
172 * otherwise returns NULL
173 *
174 * @note
175 * This modify str directly. And the 'str' is always terminated by NULL
176 * character.
177 */
178size_t qB64_decode(char *str) {
179 const char B64MAPTBL[16 * 16] = {
180 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, // 00-0F
181 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, // 10-1F
182 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 62, 64, 64, 64, 63, // 20-2F
183 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 64, 64, 64, 64, 64, 64, // 30-3F
184 64, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, // 40-4F
185 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 64, 64, 64, 64, 64, // 50-5F
186 64, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, // 60-6F
187 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 64, 64, 64, 64, 64, // 70-7F
188 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, // 80-8F
189 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, // 90-9F
190 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, // A0-AF
191 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, // B0-BF
192 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, // C0-CF
193 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, // D0-DF
194 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, // E0-EF
195 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64 // F0-FF
196 };
197
198 str = qstrreplace("tr", str, "~", "+");
199 str = qstrreplace("tr", str, "_", "/");
200 str = qstrreplace("tr", str, "^", "=");
201
202 char *pEncPt, *pBinPt = str;
203 int nIdxOfFour = 0;
204 char cLastByte = 0;
205 for (pEncPt = str; *pEncPt != '\0'; pEncPt++) {
206 char cByte = B64MAPTBL[(unsigned char) (*pEncPt)];
207 if (cByte == 64)
208 continue;
209
210 if (nIdxOfFour == 0) {
211 nIdxOfFour++;
212 } else if (nIdxOfFour == 1) {
213 // 00876543 0021????
214 //*pBinPt++ = ( ((cLastByte << 2) & 0xFC) | ((cByte >> 4) & 0x03) );
215 *pBinPt++ = ((cLastByte << 2) | (cByte >> 4));
216 nIdxOfFour++;
217 } else if (nIdxOfFour == 2) {
218 // 00??8765 004321??
219 //*pBinPt++ = ( ((cLastByte << 4) & 0xF0) | ((cByte >> 2) & 0x0F) );
220 *pBinPt++ = ((cLastByte << 4) | (cByte >> 2));
221 nIdxOfFour++;
222 } else {
223 // 00????87 00654321
224 //*pBinPt++ = ( ((cLastByte << 6) & 0xC0) | (cByte & 0x3F) );
225 *pBinPt++ = ((cLastByte << 6) | cByte);
226 nIdxOfFour = 0;
227 }
228
229 cLastByte = cByte;
230 }
231 *pBinPt = '\0';
232
233 return (pBinPt - str);
234}
235
236
237// Duplicate some small amount of code from toys/pending/sh.c
238// TODO - to be really useful I need to return the output.
239int runToy(char *argv[])
240{
241 int ret = 0;
242 struct toy_list *tl;
243 struct toy_context temp;
244 sigjmp_buf rebound;
245
246 if ((tl = toy_find(argv[0])) )//&& (tl->flags & (TOYFLAG_NOFORK|TOYFLAG_MAYFORK)))
247 {
248 // This fakes lots of what toybox_main() does.
249 memcpy(&temp, &toys, sizeof(struct toy_context));
250 memset(&toys, 0, sizeof(struct toy_context));
251
252 if (!sigsetjmp(rebound, 1))
253 {
254 toys.rebound = &rebound;
255 toy_init(tl, argv); // argv must be null terminated
256 tl->toy_main();
257 xflush(0);
258 }
259 ret = toys.exitval;
260 if (toys.optargs != toys.argv+1) free(toys.optargs);
261 if (toys.old_umask) umask(toys.old_umask);
262 memcpy(&toys, &temp, sizeof(struct toy_context));
263 }
264
265 return ret;
266}
267
268
269#undef FALSE
270#undef TRUE
271#ifndef FALSE
272// NEVER change this, true and false work to.
273typedef enum
274{
275 FALSE = 0,
276 TRUE = 1
277} boolean;
278#endif
279
280
281
282// Silly "man getrandom" is bullshitting.
283// Note - this is Linux specific, it's calling a Linux kernel function.
284// Remove this when we have a real getrandom(), and replace it with -
285// #include <sys/random.h>
286#include <sys/syscall.h>
287#include <linux/random.h>
288int getrandom(void *b, size_t l, unsigned int f)
289{
290 return (int) syscall(SYS_getrandom, b, l, f);
291}
292
293
294
295typedef struct _gridStats gridStats;
296struct _gridStats
297{
298 float next;
299 struct timeval last;
300 qhashtbl_t *stats;
301};
302
303
304typedef struct _HTMLfile HTMLfile;
305struct _HTMLfile
306{
307 struct timespec last;
308 qlist_t *fragments;
309};
310qhashtbl_t *HTMLfileCache = NULL;
311
312
313typedef struct _reqData reqData;
314
315typedef void *(*pageFunction) (char *file, reqData *Rd, HTMLfile *thisFile);
316typedef struct _dynPage dynPage;
317struct _dynPage
318{
319 char *name;
320 pageFunction func;
321};
322qhashtbl_t *dynPages;
323static void newDynPage(char *name, pageFunction func)
324{
325 dynPage *dp = xmalloc(sizeof(dynPage));
326 dp->name = name; dp->func = func;
327 dynPages->put(dynPages, dp->name, dp, sizeof(dynPage));
328 free(dp);
329}
330
331
332#define HMACSIZE EVP_MAX_MD_SIZE * 2
333#define HMACSIZE64 88
334
335// Session details about the logged in web user. A sorta state machine. Ish.
336enum reqSessionStatus // Status of the session. Refresh and wipe / nuke -> delete the old session file first.
337{
338 SHS_UNKNOWN = 0, // Haven't looked at the session yet. -> validate it
339 SHS_NONE, // No session at all. -> logout
340 SHS_BOGUS, // Looked at the session, it's bogus. -> nuke and logout
341 SHS_PROBLEM, // Some other problem with the session. -> nuke and logout
342 SHS_VALID, // Session is valid. -> continue
343
344 SHS_LOGIN, // User has just logged in, add UUID to session. -> wipe, add UUID
345
346 SHS_RENEW, // Refresh the session based on timer. -> continue
347 SHS_REFRESH, // Refresh the session for other reason. -> continue
348 SHS_IDLE, // Session has been idle too long. -> relogin
349 SHS_ANCIENT, // Session is way too old. -> nuke and logout
350
351 SHS_SECURITY, // High security task needs users name & password. ->
352 SHS_RELOGIN, // Ask user to login again. ->
353
354 SHS_KEEP, // Keep the session. -> continue
355 SHS_WIPE, // Wipe the session, use users UUID. -> continue
356 SHS_NUKE // Wipe the session, no UUID. -> logout
357};
358
359typedef struct _sesh sesh;
360struct _sesh
361{
362 char salt[256 + 1], seshID[256 + 1],
363 sesh[256 + 16 + 10 + 1], munchie[HMACSIZE + 16 + 10 + 1], toke_n_munchie[HMACSIZE + 1], hashish[HMACSIZE + 1],
364 leaf[HMACSIZE64 + 6 + 1], *UUID, *name;
365 struct timespec timeStamp[2];
366 short level;
367 enum reqSessionStatus status;
368 boolean isLinky;
369};
370
371// Details about the current web request.
372struct _reqData
373{
374 lua_State *L;
375 qhashtbl_t *configs, *queries, *body, *cookies, *headers, *valid, *stuff, *database, *Rcookies, *Rheaders;
376 char *Scheme, *Host, *Method, *Script, *Path, *RUri, *doit, *form, *output, *outQuery;
377 sesh shs, *lnk;
378 gridStats *stats;
379 qlist_t *errors, *messages;
380 qgrow_t *reply;
381 struct timespec then;
382 boolean fromDb;
383};
384
385static void showSesh(qgrow_t *reply, sesh *shs)
386{
387 if (shs->isLinky)
388 reply->addstrf(reply, "Linky:<br>\n<pre>\n");
389 else
390 reply->addstrf(reply, "Session:<br>\n<pre>\n");
391
392 if (NULL != shs->name)
393 reply->addstrf(reply, " &nbsp; name = %s\n", shs->name);
394 if (NULL != shs->UUID)
395 reply->addstrf(reply, " &nbsp; UUID = %s\n", shs->UUID);
396 reply->addstrf(reply, " &nbsp; salt = %s\n", shs->salt);
397 reply->addstrf(reply, " &nbsp; seshID = %s\n", shs->seshID);
398 reply->addstrf(reply, " &nbsp; timeStamp = %ld.%ld\n", shs->timeStamp[1].tv_sec, shs->timeStamp[1].tv_nsec);
399 reply->addstrf(reply, " &nbsp; sesh = %s\n", shs->sesh);
400 reply->addstrf(reply, " &nbsp; munchie = %s\n", shs->munchie);
401 reply->addstrf(reply, " &nbsp; toke_n_munchie = %s\n", shs->toke_n_munchie);
402 reply->addstrf(reply, " &nbsp; hashish = %s\n", shs->hashish);
403 reply->addstrf(reply, " &nbsp; leaf = %s\n", shs->leaf);
404 reply->addstrf(reply, " &nbsp; level = %d\n", (int) shs->level);
405 reply->addstr(reply, "</pre>\n");
406}
407
408
409char toybuf[4096];
410lua_State *L;
411qhashtbl_t *configs;
412MYSQL *database, *dbconn;
413unsigned int dbTimeout;
414struct timespec dbLast;
415my_bool dbReconnect;
416gridStats *stats;
417boolean isTmux = 0;
418boolean isWeb = 0;
419char *pwd = "";
420char *scRoot = "/opt/opensim_SC";
421char *scUser = "opensimsc";
422char *scBin = "";
423char *scEtc = "";
424char *scLib = "";
425char *scRun = "";
426char *scBackup = "";
427char *scCache = "";
428char *scData = "";
429char *scLog = "";
430char *Tconsole = "SledjChisl";
431char *Tsocket = "opensim-tmux.socket";
432char *Ttab = "SC";
433char *Tcmd = "tmux -S";
434char *webRoot = "/var/www/html";
435char *URL = "fcgi-bin/sledjchisl.fcgi";
436char *ToS = "Be good.";
437char *webIframers = "";
438int seshRenew = 10 * 60;
439int idleTimeOut = 30 * 60;
440int seshTimeOut = 24 * 60 * 60;
441int newbieTimeOut = 30;
442float loadAverageInc = 0.5;
443int simTimeOut = 45;
444boolean DEBUG = TRUE;
445qhashtbl_t *mimeTypes;
446qlist_t *dbRequests;
447
448
449// TODO - log to file. The problem is we don't know where to log until after we have loaded the configs, and before that we are spewing log messages.
450// Now that we are using spawn-fcgi, all the logs are going to STDERR, which we can capture and write to a file.
451// Unfortunately spawn-fcgi in deamon mode sends all the output to /dev/null or something.
452// A better idea, when we spawn tmux or spawn-fcgi, capture STDERR, full log everything to that, filtered log to the tmux console (STDOUT).
453// Then we can use STDOUT / STDIN to run the console stuff.
454
455// TODO - escape anything that will turn the console into garbage.
456
457// https://stackoverflow.com/questions/4842424/list-of-ansi-color-escape-sequences
458char *logTypes[] =
459{
460 "91;1;4", "CRITICAL", // red underlined
461 "31", "ERROR", // dark red
462 "93", "WARNING", // yellow
463 "36", "TIMEOUT", // cyan
464 "97;40", "INFO", // white
465 "90", "DEBUG", // grey
466// VERBOSE? UNKNOWN? FATAL? SILENT? All from Android aparently.
467 "35", "debug", // magenta
468 "34", "timeout", // blue
469};
470
471#define DATE_TIME_LEN 42
472void logMe(int v, char *format, ...)
473{
474 va_list va, va2;
475 int len;
476 char *ret;
477 struct timeval tv;
478 time_t curtime;
479 char date[DATE_TIME_LEN];
480
481 va_start(va, format);
482 va_copy(va2, va);
483 // How long is it?
484 len = vsnprintf(0, 0, format, va);
485 len++;
486 va_end(va);
487 // Allocate and do the sprintf()
488 ret = xmalloc(len);
489 vsnprintf(ret, len, format, va2);
490 va_end(va2);
491
492 gettimeofday(&tv, NULL);
493 curtime = tv.tv_sec;
494 strftime(date, DATE_TIME_LEN, "(%Z %z) %F %T", localtime(&curtime));
495
496 v *= 2;
497 fprintf(stderr, "%s.%.6ld \e[%sm%-8s sledjchisl: %s\e[0m\n", date, tv.tv_usec, logTypes[v], logTypes[v + 1], ret);
498 free(ret);
499}
500#define C(...) logMe(0, __VA_ARGS__)
501#define E(...) logMe(1, __VA_ARGS__)
502#define W(...) logMe(2, __VA_ARGS__)
503#define T(...) logMe(3, __VA_ARGS__)
504#define I(...) logMe(4, __VA_ARGS__)
505#define D(...) logMe(5, __VA_ARGS__)
506#define d(...) logMe(6, __VA_ARGS__)
507#define t(...) logMe(7, __VA_ARGS__)
508
509
510static void addStrL(qlist_t *list, char *s)
511{
512 list->addlast(list, s, strlen(s) + 1);
513}
514
515static char *getStrH(qhashtbl_t *hash, char *key)
516{
517 char *ret = "", *t;
518
519 t = hash->getstr(hash, key, false);
520 if (NULL != t)
521 ret = t;
522 return ret;
523}
524
525
526char *myHMAC(char *in, boolean b64)
527{
528 EVP_MD_CTX *mdctx = EVP_MD_CTX_create(); // Gets renamed to EVP_MD_CTX_new() in later versions.
529 unsigned char md_value[EVP_MAX_MD_SIZE];
530 unsigned int md_len;
531
532 EVP_DigestInit_ex(mdctx, EVP_sha512(), NULL); // EVP_sha3_512() isn't available until later versions.
533 EVP_DigestUpdate(mdctx, in, strlen(in));
534 EVP_DigestFinal_ex(mdctx, md_value, &md_len);
535 EVP_MD_CTX_destroy(mdctx); // Gets renamed to EVP_MD_CTX_free() in later versions.
536
537 if (b64)
538 return qB64_encode(md_value, md_len);
539 else
540 return qhex_encode(md_value, md_len);
541}
542
543char *myHMACkey(char *key, char *in, boolean b64)
544{
545 unsigned char md_value[EVP_MAX_MD_SIZE];
546 unsigned int md_len;
547 unsigned char* digest = HMAC(EVP_sha512(), key, strlen(key), (unsigned char *) in, strlen(in), md_value, &md_len);
548
549 if (b64)
550 return qB64_encode(md_value, md_len);
551 else
552 return qhex_encode(md_value, md_len);
553}
554
555
556// In Lua 5.0 reference manual is a table traversal example at page 29.
557void PrintTable(lua_State *L)
558{
559 lua_pushnil(L);
560
561 while (lua_next(L, -2) != 0)
562 {
563 // Numbers can convert to strings, so check for numbers before checking for strings.
564 if (lua_isnumber(L, -1))
565 printf("%s = %f\n", lua_tostring(L, -2), lua_tonumber(L, -1));
566 else if (lua_isstring(L, -1))
567 printf("%s = '%s'\n", lua_tostring(L, -2), lua_tostring(L, -1));
568 else if (lua_istable(L, -1))
569 PrintTable(L);
570 lua_pop(L, 1);
571 }
572}
573
574
575int sendTmuxKeys(char *dest, char *keys)
576{
577 int ret = 0, i;
578 char *c = xmprintf("%s %s/%s send-keys -t %s:%s '%s'", Tcmd, scRun, Tsocket, Tconsole, dest, keys);
579
580 i = system(c);
581 if (!WIFEXITED(i))
582 E("tmux send-keys command failed!");
583 free(c);
584 return ret;
585}
586
587int sendTmuxCmd(char *dest, char *cmd)
588{
589 int ret = 0, i;
590 char *c = xmprintf("%s %s/%s send-keys -t %s:'%s' '%s' Enter", Tcmd, scRun, Tsocket, Tconsole, dest, cmd);
591
592 i = system(c);
593 if (!WIFEXITED(i))
594 E("tmux send-keys command failed!");
595 free(c);
596 return ret;
597}
598
599void waitTmuxText(char *dest, char *text)
600{
601 int i;
602 // Using " for the grep pattern, coz ' might be used in a sim name.
603// TODO - should escape \ " ` in text.
604 char *c = xmprintf("sleep 5; %s %s/%s capture-pane -t %s:'%s' -p | grep -F \"%s\" 2>&1 > /dev/null", Tcmd, scRun, Tsocket, Tconsole, dest, text);
605
606 D("Waiting for '%s'.", text);
607 do
608 {
609 i = system(c);
610 if (!WIFEXITED(i))
611 {
612 E("tmux capture-pane command failed!");
613 break;
614 }
615 else if (0 == WEXITSTATUS(i))
616 break;
617 } while (1);
618
619 free(c);
620}
621
622float waitLoadAverage(float la, float extra, int timeout)
623{
624 struct sysinfo info;
625 struct timespec timeOut;
626 float l;
627 int to = timeout;
628
629 T("Sleeping until load average is below %.02f (%.02f + %.02f) or for %d seconds.", la + extra, la, extra, timeout);
630 clock_gettime(CLOCK_MONOTONIC, &timeOut);
631 to += timeOut.tv_sec;
632
633 do
634 {
635 msleep(5000);
636 sysinfo(&info);
637 l = info.loads[0]/65536.0;
638 clock_gettime(CLOCK_MONOTONIC, &timeOut);
639 timeout -= 5;
640 t("Tick, load average is %.02f, countdown %d seconds.", l, timeout);
641 } while (((la + extra) < l) && (timeOut.tv_sec < to));
642
643 return l;
644}
645
646
647// Rob forget to do this, but at least he didn't declare it static.
648struct dirtree *dirtree_handle_callback(struct dirtree *new, int (*callback)(struct dirtree *node));
649
650typedef struct _simList simList;
651struct _simList
652{
653 int len, num;
654 char **sims;
655};
656
657static int filterSims(struct dirtree *node)
658{
659 if (!node->parent) return DIRTREE_RECURSE | DIRTREE_SHUTUP;
660 if ((strncmp(node->name, "sim", 3) == 0) && ((strcmp(node->name, "sim_skeleton") != 0)))
661 {
662 simList *list = (simList *) node->parent->extra;
663
664 if ((list->num + 1) > list->len)
665 {
666 list->len = list->len + 1;
667 list->sims = xrealloc(list->sims, list->len * sizeof(char *));
668 }
669 list->sims[list->num] = xstrdup(node->name);
670 list->num++;
671 }
672 return 0;
673}
674
675// We particularly don't want \ " `
676char *cleanSimName(char *name)
677{
678 size_t l = strlen(name);
679 char *ret = xmalloc(l + 1);
680 int i, j = 0;
681
682 for (i = 0; i < l; i++)
683 {
684 char r = name[i];
685
686 if ((' ' == r) || (isalnum(r) != 0))
687 ret[j++] = r;
688 }
689 ret[j] = '\0';
690
691 return ret;
692}
693
694simList *getSims()
695{
696 simList *sims = xmalloc(sizeof(simList));
697 memset(sims, 0, sizeof(simList));
698 char *path = xmprintf("%s/config", scRoot);
699 struct dirtree *new = dirtree_add_node(0, path, 0);
700 new->extra = (long) sims;
701 dirtree_handle_callback(new, filterSims);
702
703 qsort(sims->sims, sims->num, sizeof(char *), qstrcmp);
704 free(path);
705 return sims;
706}
707
708void freeSimList(simList *sims)
709{
710 int i;
711
712 for (i = 0; i < sims->num; i++)
713 free(sims->sims[i]);
714 free(sims->sims);
715 free(sims);
716}
717
718static int filterInis(struct dirtree *node)
719{
720 if (!node->parent) return DIRTREE_RECURSE | DIRTREE_SHUTUP;
721 int l = strlen(node->name);
722 if (strncmp(&(node->name[l - 4]), ".ini", 4) == 0)
723 {
724 strcpy((char *) node->parent->extra, node->name);
725 return DIRTREE_ABORT;
726 }
727 return 0;
728}
729
730char *getSimName(char *sim)
731{
732 char *ret = NULL;
733 char *c = xmprintf("%s/config/%s", scRoot, sim);
734 struct dirtree *new = dirtree_add_node(0, c, 0);
735
736 free(c);
737 c = xzalloc(1024);
738 new->extra = (long) c;
739 dirtree_handle_callback(new, filterInis);
740 if ('\0' != c[0])
741 {
742 char *temp = NULL;
743 regex_t pat;
744 regmatch_t m[2];
745 long len;
746 int fd;
747
748 temp = xmprintf("%s/config/%s/%s", scRoot, sim, c);
749 fd = xopenro(temp);
750 xregcomp(&pat, "RegionName = \"(.+)\"", REG_EXTENDED);
751 do
752 {
753 // TODO - get_line() is slow, and wont help much with DOS and Mac line endings.
754 // gio_gets() isn't any faster really, but deals with DOS line endings at least.
755 free(temp);
756 temp = get_line(fd);
757 if (temp)
758 {
759 if (!regexec(&pat, temp, 2, m, 0))
760 {
761 // Return first parenthesized subexpression as string.
762 if (pat.re_nsub > 0)
763 {
764 ret = xmprintf("%.*s", (int) (m[1].rm_eo - m[1].rm_so), temp + m[1].rm_so);
765 free(temp);
766 break;
767 }
768 }
769 }
770 } while (temp);
771 regfree(&pat);
772 xclose(fd);
773 }
774 free(c);
775 return ret;
776}
777
778
779// Expects either "simXX" or "ROBUST".
780int checkSimIsRunning(char *sim)
781{
782 int ret = 0;
783 struct stat st;
784
785 // Check if it's running.
786 char *path = xmprintf("%s/caches/%s.pid", scRoot, sim);
787 if (0 == stat(path, &st))
788 {
789 int fd, i;
790 char *pid = NULL;
791
792 // Double check if it's REALLY running.
793 if ((fd = xopenro(path)) == -1)
794 perror_msg("xopenro(%s)", path);
795 else
796 {
797 pid = get_line(fd);
798 if (NULL == pid)
799 perror_msg("get_line(%s)", path);
800 else
801 {
802 xclose(fd);
803
804// I'd rather re-use the toysh command running stuff, since ps is a toy, but that's private.
805// TODO - switch to toybox ps and rm.
806 free(path);
807 path = xmprintf("ps -p %s --no-headers -o comm", pid);
808 i = system(path);
809 if (WIFEXITED(i))
810 {
811 if (0 != WEXITSTATUS(i)) // No such pid.
812 {
813 free(path);
814 path = xmprintf("rm -f %s/caches/%s.pid", scRoot, sim);
815 d("%s", path);
816 i = system(path);
817 }
818 else
819 d("checkSimIsRunning(%s) has PID %s, which is actually running.", sim, pid);
820 }
821 }
822 }
823 free(pid);
824 }
825
826 // Now check if it's really really running. lol
827 free(path);
828 path = xmprintf("%s/caches/%s.pid", scRoot, sim);
829 if (0 == stat(path, &st))
830 {
831 D("checkSimIsRunning(%s) -> %s is really really running.", sim, path);
832 ret = 1;
833 }
834 else
835 D("checkSimIsRunning(%s) -> %s is not running.", sim, path);
836
837 free(path);
838 return ret;
839}
840
841
842static void PrintEnv(qgrow_t *reply, char *label, char **envp)
843{
844 reply->addstrf(reply, "%s:<br>\n<pre>\n", label);
845 for ( ; *envp != NULL; envp++)
846 reply->addstrf(reply, "%s\n", *envp);
847 reply->addstr(reply, "</pre>\n");
848}
849
850static void printEnv(char **envp)
851{
852 for ( ; *envp != NULL; envp++)
853 D("%s", *envp);
854}
855
856
857typedef struct _rowData rowData;
858struct _rowData
859{
860 char **fieldNames;
861 qlist_t *rows;
862};
863
864static void dumpHash(qhashtbl_t *tbl)
865{
866 qhashtbl_obj_t obj;
867
868 memset((void*)&obj, 0, sizeof(obj));
869 tbl->lock(tbl);
870 while(tbl->getnext(tbl, &obj, true) == true)
871 d("%s = %s", obj.name, (char *) obj.data);
872 tbl->unlock(tbl);
873}
874
875static void dumpArray(int d, char **ar)
876{
877 int i = 0;
878
879 while (ar[i] != NULL)
880 {
881 d("%d %d %s", d, i, ar[i]);
882 i++;
883 }
884}
885
886
887
888typedef struct _dbFields dbFields;
889struct _dbFields
890{
891 qlisttbl_t *flds;
892 int count;
893};
894typedef struct _dbField dbField;
895struct _dbField
896{
897 char *name;
898 enum enum_field_types type;
899 unsigned long length;
900 unsigned int flags;
901 unsigned int decimals;
902};
903
904void dbFreeFields(dbFields *flds, boolean all)
905{
906 flds->count--;
907
908// TODO - sigh, looks to be inconsistant why some do and some don't leak.
909// I guess the ones that don't leak are the ones that crash?
910// It's only a tiny leak anyway, 80 bytes total.
911// if ((0 >= flds->count) || all) // CRASHY
912 if ((0 >= flds->count)) // LEAKY
913 {
914 qlisttbl_obj_t obj;
915
916 memset((void *) &obj, 0, sizeof(obj));
917 flds->flds->lock(flds->flds);
918 while(flds->flds->getnext(flds->flds, &obj, NULL, false) == true)
919 {
920 dbField *fld = (dbField *) obj.data;
921 free(fld->name);
922 }
923 flds->flds->unlock(flds->flds);
924 flds->flds->free(flds->flds);
925 flds->flds = NULL;
926 free(flds);
927 }
928}
929
930enum dbCommandType
931{
932 CT_SELECT,
933 CT_CREATE,
934 CT_UPDATE,
935 CT_NONE
936};
937
938typedef struct _dbRequest dbRequest;
939struct _dbRequest
940{
941 char *table, *join, *where, *order, *sql;
942 MYSQL_STMT *prep; // NOTE - executing it stores state in this.
943 dbFields *fields;
944 int inCount, outCount, rowCount;
945 char **inParams, **outParams;
946 MYSQL_BIND *inBind, *outBind;
947 rowData *rows;
948 my_ulonglong count;
949 enum dbCommandType type;
950 boolean freeOutParams;
951};
952
953void dbFreeRequest(dbRequest *req, boolean all)
954{
955 int i;
956
957 D("Cleaning up prepared database request %s - %s %d %d", req->table, req->where, req->outCount, req->inCount);
958
959 if (NULL != req->outBind)
960 {
961 for (i = 0; i < req->outCount; i++)
962 {
963 if (NULL != req->outBind[i].buffer) free(req->outBind[i].buffer);
964 if (NULL != req->outBind[i].length) free(req->outBind[i].length);
965 if (NULL != req->outBind[i].error) free(req->outBind[i].error);
966 if (NULL != req->outBind[i].is_null) free(req->outBind[i].is_null);
967 }
968 free(req->outBind);
969 req->outBind = NULL;
970 }
971 else
972 D(" No out binds to clean up for %s - %s.", req->table, req->where);
973 if (NULL != req->inBind)
974 {
975 for (i = 0; i < req->inCount; i++)
976 {
977 if (NULL != req->inBind[i].buffer) free(req->inBind[i].buffer);
978 if (NULL != req->inBind[i].length) free(req->inBind[i].length);
979 if (NULL != req->inBind[i].error) free(req->inBind[i].error);
980 if (NULL != req->inBind[i].is_null) free(req->inBind[i].is_null);
981 }
982 free(req->inBind);
983 req->inBind = NULL;
984 }
985 else
986 D(" No in binds to clean up for %s - %s.", req->table, req->where);
987
988 if (req->freeOutParams && all)
989 {
990 if (NULL != req->outParams)
991 {
992 free(req->outParams);
993 req->outParams = NULL;
994 }
995 else
996 D(" No out params to clean up for %s - %s.", req->table, req->where);
997 }
998 if (NULL != req->sql) free(req->sql);
999 else
1000 D(" No SQL to clean up for %s - %s.", req->table, req->where);
1001 req->sql = NULL;
1002 if (NULL != req->prep)
1003 {
1004 if (0 != mysql_stmt_close(req->prep))
1005 C(" Unable to close the prepared statement!");
1006 req->prep = NULL;
1007 }
1008
1009 if (all)
1010 {
1011 if (NULL != req->fields)
1012 {
1013 dbFreeFields(req->fields, all);
1014 req->fields = NULL;
1015 }
1016 else
1017 D(" No fields to clean up for %s - %s.", req->table, req->where);
1018 }
1019}
1020
1021void freeDb(boolean all)
1022{
1023 dbRequest **rq;
1024
1025 if (dbRequests)
1026 {
1027 if (all)
1028 {
1029 while (NULL != (rq = (dbRequest **) dbRequests->popfirst(dbRequests, NULL)))
1030 {
1031 dbFreeRequest(*rq, all);
1032 free(rq);
1033 }
1034 dbRequests->free(dbRequests);
1035 dbRequests = NULL;
1036 }
1037 else
1038 {
1039 qlist_obj_t obj;
1040
1041 memset((void*)&obj, 0, sizeof(obj)); // must be cleared before call
1042 dbRequests->lock(dbRequests);
1043 while (dbRequests->getnext(dbRequests, &obj, false) == true)
1044 dbFreeRequest(*((dbRequest **) obj.data), all);
1045 dbRequests->unlock(dbRequests);
1046 }
1047 }
1048
1049 if (database) mysql_close(database);
1050 database = NULL;
1051 mysql_library_end();
1052}
1053
1054static boolean dbConnect()
1055{
1056 database = mysql_init(NULL);
1057 if (NULL == database)
1058 {
1059 E("mysql_init() failed - %s", mysql_error(database));
1060 return FALSE;
1061 }
1062
1063/* TODO - dammit, no mysql_get_option(), MariaDB docs say mysql_get_optionv(), which doesn't exist either.
1064 Says "This function was added in MariaDB Connector/C 3.0.0.", I have MariaDB / MySQL client version: 10.1.44-MariaDB.
1065
1066 if (mysql_get_option(database, MYSQL_OPT_CONNECT_TIMEOUT, &dbTimeout))
1067 E("mysql_get_option(MYSQL_OPT_CONNECT_TIMEOUT) failed - %s", mysql_error(database));
1068 else
1069 D("Database MYSQL_OPT_CONNECT_TIMEOUT = %d", dbTimeout);
1070
1071 if (mysql_get_option(database, MYSQL_OPT_RECONNECT, &dbReconnect))
1072 E("mysql_get_option(MYSQL_OPT_RECONNECT) failed - %s", mysql_error(database));
1073 else
1074 D("Database MYSQL_OPT_RECONNECT = %d", (int) dbReconnect);
1075*/
1076
1077 // Seems best to disable auto-reconnect, so I have more control over reconnections.
1078 dbReconnect = 0;
1079 if (mysql_options(database, MYSQL_OPT_RECONNECT, &dbReconnect))
1080 E("mysql_options(MYSQL_OPT_RECONNECT) failed - %s", mysql_error(database));
1081 else
1082 D("Database MYSQL_OPT_RECONNECT is now %d", (int) dbReconnect);
1083
1084 dbconn = mysql_real_connect(database,
1085 getStrH(configs, "Data Source"),
1086 getStrH(configs, "User ID"),
1087 getStrH(configs, "Password"),
1088 getStrH(configs, "Database"),
1089// 3036, "/var/run/mysqld/mysqld.sock",
1090 0, NULL,
1091 CLIENT_FOUND_ROWS | CLIENT_LOCAL_FILES | CLIENT_MULTI_STATEMENTS | CLIENT_MULTI_RESULTS);
1092 if (NULL == dbconn)
1093 {
1094 E("mysql_real_connect() failed - %s", mysql_error(database));
1095 return FALSE;
1096 }
1097
1098 // Just set the fucking thing to a year. Pffft.
1099 dbTimeout = 60 * 60 * 24 * 7 * 52;
1100 char *sql = xmprintf("SET SESSION wait_timeout=%d", (int) dbTimeout);
1101
1102 if (mysql_query(database, sql))
1103 E("SET SESSION wait_timeout=%d failed - %s", (int) dbTimeout, mysql_error(database));
1104 else
1105 D("Database wait_timeout = %d", (int) dbTimeout);
1106 free(sql);
1107
1108 if (-1 == clock_gettime(CLOCK_REALTIME, &dbLast))
1109 perror_msg("Unable to get the time.");
1110
1111 return TRUE;
1112}
1113
1114// A general error function that checks for certain errors that mean we should try to connect to the server MariaDB again.
1115// https://mariadb.com/kb/en/mariadb-error-codes/
1116// 1129? 1152? 1184? 1218? 1927 3032? 4150?
1117// "server has gone away" isn't listed there, that's the one I was getting. Pffft
1118// It's 2006, https://dev.mysql.com/doc/refman/8.0/en/gone-away.html
1119// Ah it could be "connection inactive for 8 hours".
1120// Which might be why OpenSim opens a new connection for EVERYTHING.
1121// https://dev.mysql.com/doc/refman/5.7/en/c-api-auto-reconnect.html
1122// Has more details.
1123static boolean dbCheckError(char *error, char *sql)
1124{
1125 int e = mysql_errno(database);
1126
1127 E("MariaDB error %d - %s: %s\n%s", e, error, mysql_error(database), sql);
1128 if (2006 == e)
1129 {
1130 W("Reconnecting to database.");
1131 freeDb(false);
1132 return dbConnect();
1133 }
1134
1135 return FALSE;
1136}
1137// "Statement execute failed 2013: Lost connection to MySQL server during query"
1138static boolean dbStmtCheckError(dbRequest *req, char *error, char *sql)
1139{
1140 int e = mysql_stmt_errno(req->prep);
1141
1142 E("MariaDB prepared statement error %d - %s: %s\n%s", e, error, mysql_stmt_error(req->prep), sql);
1143 if (2013 == e)
1144 {
1145 W("Reconnecting to database.");
1146 freeDb(false);
1147 return dbConnect();
1148 }
1149
1150 return FALSE;
1151}
1152
1153dbFields *dbGetFields(char *table)
1154{
1155 static qhashtbl_t *tables = NULL;
1156 if (NULL == tables) tables = qhashtbl(0, 0);
1157 dbFields *ret = tables->get(tables, table, NULL, false);
1158
1159 if (NULL == ret)
1160 {
1161 // Seems the only way to get field metadata is to actually perform a SQL statement, then you get the field metadata for the result set.
1162 // Chicken, meet egg, sorry you had to cross the road for this.
1163 char *sql = xmprintf("SELECT * FROM %s LIMIT 0", table);
1164
1165d("Getting field metadata for %s", table);
1166 if (mysql_query(database, sql))
1167 {
1168// E("MariaDB error %d - Query failed 0: %s\n%s", mysql_errno(database), mysql_error(database), sql);
1169 if (dbCheckError("Query failed 0", sql))
1170 {
1171 ret = dbGetFields(table);
1172 free(sql);
1173 return ret;
1174 }
1175 }
1176 else
1177 {
1178 MYSQL_RES *res = mysql_store_result(database);
1179
1180 if (!res)
1181 E("MariaDB error %d - Couldn't get results set from %s\n %s", mysql_errno(database), mysql_error(database), sql);
1182 else
1183 {
1184 MYSQL_FIELD *fields = mysql_fetch_fields(res);
1185
1186 if (!fields)
1187 E("MariaDB error %d - Failed fetching fields: %s", mysql_errno(database), mysql_error(database));
1188 else
1189 {
1190 unsigned int i, num_fields = mysql_num_fields(res);
1191
1192 ret = xmalloc(sizeof(dbFields)); // Little bit LEAKY
1193 ret->flds = qlisttbl(QLISTTBL_UNIQUE | QLISTTBL_LOOKUPFORWARD);
1194 ret->count = 1;
1195 for (i = 0; i < num_fields; i++)
1196 {
1197 dbField *fld = xmalloc(sizeof(dbField));
1198 fld->name = xstrdup(fields[i].name);
1199 fld->type = fields[i].type;
1200 fld->length = fields[i].length;
1201 fld->flags = fields[i].flags;
1202 fld->decimals = fields[i].decimals;
1203 ret->flds->put(ret->flds, fld->name, fld, sizeof(*fld));
1204 free(fld);
1205 }
1206 tables->put(tables, table, ret, sizeof(*ret));
1207 }
1208 mysql_free_result(res);
1209 }
1210 }
1211 free(sql);
1212 }
1213 else // Reference count these, coz some tables are used more than once.
1214 ret->count++;
1215
1216 return ret;
1217}
1218
1219
1220/* How to deal with prepared SQL statements.
1221http://karlssonondatabases.blogspot.com/2010/07/prepared-statements-are-they-useful-or.html
1222https://blog.cotten.io/a-taste-of-mysql-in-c-87c5de84a31d?gi=ab3dd1425b29
1223https://raspberry-projects.com/pi/programming-in-c/databases-programming-in-c/mysql/accessing-the-database
1224
1225IG and CG now both have sims connected to other grids, so some sort of
1226multi database solution would be good, then we can run the grid and the
1227external sims all in one.
1228
1229Not sure if this'll work with Count(*).
1230
1231---------------------------------------------
1232
1233The complicated bit is the binds.
1234
1235You are binding field values to C memory locations.
1236The parameters and returned fields need binds.
1237Mostly seems to be the value parts of the SQL statements.
1238
1239I suspect most will be of the form -
1240 ... WHERE x=? and foo=?
1241 INSERT INTO table VALUES (?,?,?)
1242 UPDATE table SET x=?, foo=? WHERE id=?
1243
1244 A multi table update -
1245 UPDATE items,month SET items.price=month.price WHERE items.id=month.id;
1246*/
1247
1248int dbDoSomething(dbRequest *req, boolean count, ...)
1249{
1250 int ret = 0;
1251 va_list ap;
1252 struct timespec then;
1253 int i, j;
1254 MYSQL_RES *prepare_meta_result = NULL;
1255
1256 if (-1 == clock_gettime(CLOCK_REALTIME, &then))
1257 perror_msg("Unable to get the time.");
1258
1259// TODO - should factor this out to it's own function, and call that function in dbCount() and dbCountJoin().
1260// Or better yet, finally migrate those functions to using dbDoSomething().
1261 double n = (dbLast.tv_sec * 1000000000.0) + dbLast.tv_nsec;
1262 double t = (then.tv_sec * 1000000000.0) + then.tv_nsec;
1263
1264t("Database timeout test %lf > %lf", ((t - n) / 1000000000.0), (dbTimeout / 2.0));
1265 if (((t - n) / 1000000000.0) > (dbTimeout / 2.0))
1266 {
1267 T("Avoid database timeout of %d seconds, pinging it.", dbTimeout);
1268 if (0 != mysql_ping(database))
1269 {
1270 W("Reconnecting to database.");
1271 freeDb(false);
1272 dbConnect();
1273 }
1274 }
1275
1276 va_start(ap, count);
1277
1278 if (NULL == req->prep)
1279 {
1280 D("Creating prepared statement for %s - %s", req->table, req->where);
1281
1282 if (0 == req->type)
1283 req->type = CT_SELECT;
1284
1285 req->fields = dbGetFields(req->table);
1286 if (NULL == req->fields)
1287 {
1288 E("Unknown fields for table %s.", req->table);
1289 ret++;
1290 goto end;
1291 }
1292
1293 switch (req->type)
1294 {
1295 case CT_SELECT :
1296 {
1297 char *select = xmprintf("");
1298
1299 i = 0;
1300 while (req->outParams[i] != NULL)
1301 {
1302 char *t = xmprintf("%s,%s", select, req->outParams[i]);
1303 free(select);
1304 select = t;
1305 i++;
1306 }
1307 if (0 == i)
1308 {
1309 free(select);
1310 if (count)
1311 select = xmprintf(",Count(*)");
1312 else
1313 select = xmprintf(",*");
1314 }
1315
1316 if (NULL == req->join)
1317 req->join = "";
1318
1319 if (req->where)
1320 req->sql = xmprintf("SELECT %s FROM %s %s WHERE %s", &select[1], req->table, req->join, req->where);
1321 else
1322 req->sql = xmprintf("SELECT %s FROM %s", &select[1], req->table, req->join);
1323 free(select);
1324 if (req->order)
1325 {
1326 char *t = xmprintf("%s ORDER BY %s", req->sql, req->order);
1327
1328 free(req->sql);
1329 req->sql = t;
1330 }
1331 break;
1332 }
1333
1334 case CT_CREATE :
1335 {
1336 char *values = xmprintf("");
1337
1338 i = 0;
1339 while (req->inParams[i] != NULL)
1340 {
1341 char *t = xmprintf("%s, %s=?", values, req->inParams[i]);
1342 free(values);
1343 values = t;
1344 i++;
1345 }
1346 if (0 == i)
1347 {
1348 E("Statement prepare for INSERT must have in paramaters.");
1349 ret++;
1350 free(values);
1351 values = xmprintf("");
1352 }
1353 req->sql = xmprintf("INSERT INTO %s SET %s", req->table, &values[1]);
1354 free(values);
1355
1356 break;
1357 }
1358
1359 case CT_UPDATE :
1360 {
1361 break;
1362 }
1363
1364 case CT_NONE :
1365 {
1366 W("No SQL type!");
1367 break;
1368 }
1369 }
1370
1371d("New SQL statement - %s", req->sql);
1372 // prepare statement with the other fields
1373 req->prep = mysql_stmt_init(database);
1374 if (NULL == req->prep)
1375 {
1376 E("Statement prepare init failed: %s\n", mysql_stmt_error(req->prep));
1377 ret++;
1378 goto end;
1379 }
1380 if (mysql_stmt_prepare(req->prep, req->sql, strlen(req->sql)))
1381 {
1382 E("Statement prepare failed: %s\n", mysql_stmt_error(req->prep));
1383 ret++;
1384 goto end;
1385 }
1386
1387 // setup the bind stuff for any "?" parameters in the SQL.
1388 req->inCount = mysql_stmt_param_count(req->prep);
1389 i = 0;
1390 while (req->inParams[i] != NULL)
1391 i++;
1392 if (i != req->inCount)
1393 {
1394 E("In parameters count don't match %d != %d for - %s", i, req->inCount, req->sql);
1395 ret++;
1396 goto freeIt;
1397 }
1398 req->inBind = xzalloc(i * sizeof(MYSQL_BIND));
1399//W("Allocated %d %d inBinds for %s", i, req->inCount, req->sql);
1400 for (i = 0; i < req->inCount; i++)
1401 {
1402 dbField *fld = req->fields->flds->get(req->fields->flds, req->inParams[i], NULL, false);
1403
1404 if (NULL == fld)
1405 {
1406 E("Unknown input field %d %s.%s for - %s", i, req->table, req->inParams[i], req->sql);
1407 ret++;
1408 goto freeIt;
1409 }
1410 else
1411 {
1412 // https://blog.cotten.io/a-taste-of-mysql-in-c-87c5de84a31d?gi=ab3dd1425b29
1413 // For some gotchas about all of this binding bit.
1414 req->inBind[i].buffer_type = fld->type;
1415 req->inBind[i].buffer = xzalloc(fld->length + 1); // Note the + 1 is for string types, and a waste for the rest.
1416 req->inBind[i].buffer_length = fld->length + 1;
1417 switch(fld->type)
1418 {
1419 case MYSQL_TYPE_TINY:
1420 {
1421//d("TINY %d %s %d", i, fld->name, req->inBind[i].buffer_length);
1422 break;
1423 }
1424
1425 case MYSQL_TYPE_SHORT:
1426 {
1427 req->inBind[i].is_unsigned = FALSE;
1428//d("SHORT %d %s %d", i, fld->name, req->inBind[i].buffer_length);
1429 break;
1430 }
1431
1432 case MYSQL_TYPE_INT24:
1433 {
1434 req->inBind[i].is_unsigned = FALSE;
1435//d("INT24 %d %s %d", i, fld->name, req->inBind[i].buffer_length);
1436 break;
1437 }
1438
1439 case MYSQL_TYPE_LONG:
1440 {
1441 req->inBind[i].is_unsigned = FALSE;
1442//d("LONG %d %s %d", i, fld->name, req->inBind[i].buffer_length);
1443 break;
1444 }
1445
1446 case MYSQL_TYPE_LONGLONG:
1447 {
1448 req->inBind[i].is_unsigned = FALSE;
1449//d("LONGLONG %d %s %d", i, fld->name, req->inBind[i].buffer_length);
1450 break;
1451 }
1452
1453 case MYSQL_TYPE_FLOAT:
1454 {
1455//d("FLOAT %d %s %d", i, fld->name, req->inBind[i].buffer_length);
1456 break;
1457 }
1458
1459 case MYSQL_TYPE_DOUBLE:
1460 {
1461//d("DOUBLE %d %s %d", i, fld->name, req->inBind[i].buffer_length);
1462 break;
1463 }
1464
1465 case MYSQL_TYPE_NEWDECIMAL:
1466 {
1467//d("NEWDECIMAL %d %s %d", i, fld->name, req->inBind[i].buffer_length);
1468 break;
1469 }
1470
1471 case MYSQL_TYPE_TIME:
1472 case MYSQL_TYPE_DATE:
1473 case MYSQL_TYPE_DATETIME:
1474 case MYSQL_TYPE_TIMESTAMP:
1475 {
1476//d("DATE / TIME ish %d %s %d", i, fld->name, req->inBind[i].buffer_length);
1477 break;
1478 }
1479
1480 case MYSQL_TYPE_STRING:
1481 case MYSQL_TYPE_VAR_STRING:
1482 {
1483//d("STRING / VARSTRING %d %s %d", i, fld->name, req->inBind[i].buffer_length);
1484 req->inBind[i].is_null = xzalloc(sizeof(my_bool));
1485 req->inBind[i].length = xzalloc(sizeof(unsigned long));
1486 break;
1487 }
1488
1489 case MYSQL_TYPE_TINY_BLOB:
1490 case MYSQL_TYPE_BLOB:
1491 case MYSQL_TYPE_MEDIUM_BLOB:
1492 case MYSQL_TYPE_LONG_BLOB:
1493 {
1494//d("BLOBs %d %s %d", i, fld->name, req->inBind[i].buffer_length);
1495 req->inBind[i].is_null = xzalloc(sizeof(my_bool));
1496 break;
1497 }
1498
1499 case MYSQL_TYPE_BIT:
1500 {
1501 req->inBind[i].is_null = xzalloc(sizeof(my_bool));
1502//d("BIT %d %s %d", i, fld->name, req->inBind[i].buffer_length);
1503 break;
1504 }
1505
1506 case MYSQL_TYPE_NULL:
1507 {
1508//d("NULL %d %s %d", i, fld->name, req->inBind[i].buffer_length);
1509 break;
1510 }
1511 }
1512 }
1513 }
1514
1515// TODO - if this is not a count, setup result bind paramateres, may be needed for counts as well.
1516 if (CT_SELECT == req->type)
1517 {
1518 prepare_meta_result = mysql_stmt_result_metadata(req->prep);
1519 if (!prepare_meta_result)
1520 {
1521 E(" mysql_stmt_result_metadata() error %d, returned no meta information - %s\n", mysql_stmt_errno(req->prep), mysql_stmt_error(req->prep));
1522 ret++;
1523 goto freeIt;
1524 }
1525 }
1526
1527 if (count)
1528 {
1529I("count!!!!!!!!!!!!!!!!");
1530 }
1531 else if (CT_SELECT == req->type)
1532 {
1533 req->outCount = mysql_num_fields(prepare_meta_result);
1534 i = 0;
1535 while (req->outParams[i] != NULL)
1536 i++;
1537 if (0 == i) // Passing in {NULL} as req->outParams means "return all of them".
1538 {
1539 req->outParams = xzalloc((req->outCount + 1) * sizeof(char *));
1540 req->freeOutParams = TRUE;
1541 qlisttbl_obj_t obj;
1542 memset((void*)&obj, 0, sizeof(obj));
1543 req->fields->flds->lock(req->fields->flds);
1544 while (req->fields->flds->getnext(req->fields->flds, &obj, NULL, false) == true)
1545 {
1546 dbField *fld = (dbField *) obj.data;
1547 req->outParams[i] = fld->name;
1548 i++;
1549 }
1550 req->outParams[i] = NULL;
1551 req->fields->flds->unlock(req->fields->flds);
1552 }
1553 if (i != req->outCount)
1554 {
1555 E("Out parameters count doesn't match %d != %d foqr - %s", i, req->outCount, req->sql);
1556 ret++;
1557 goto freeIt;
1558 }
1559 req->outBind = xzalloc(i * sizeof(MYSQL_BIND));
1560//W("Allocated %d %d outBinds for %s", i, req->outCount, req->sql);
1561 for (i = 0; i < req->outCount; i++)
1562 {
1563 dbField *fld = req->fields->flds->get(req->fields->flds, req->outParams[i], NULL, false);
1564
1565 if (NULL == fld)
1566 {
1567 E("Unknown output field %d %s.%s foqr - %s", i, req->table, req->outParams[i], req->sql);
1568 ret++;
1569 goto freeIt;
1570 }
1571 else
1572 {
1573 // https://blog.cotten.io/a-taste-of-mysql-in-c-87c5de84a31d?gi=ab3dd1425b29
1574 // For some gotchas about all of this binding bit.
1575 req->outBind[i].buffer_type = fld->type;
1576 req->outBind[i].buffer = xzalloc(fld->length + 1); // Note the + 1 is for string types, and a waste for the rest.
1577 req->outBind[i].buffer_length = fld->length + 1;
1578 req->outBind[i].error = xzalloc(sizeof(my_bool));
1579 req->outBind[i].is_null = xzalloc(sizeof(my_bool));
1580 switch(fld->type)
1581 {
1582 case MYSQL_TYPE_TINY:
1583 {
1584//d("TINY %d %s %d", i, fld->name, req->outBind[i].buffer_length);
1585 break;
1586 }
1587
1588 case MYSQL_TYPE_SHORT:
1589 {
1590//d("SHORT %s %d", fld->name, req->outBind[i].buffer_length);
1591 req->outBind[i].is_unsigned = FALSE;
1592 break;
1593 }
1594
1595 case MYSQL_TYPE_INT24:
1596 {
1597//d("INT24 %s %d", fld->name, req->outBind[i].buffer_length);
1598 req->outBind[i].is_unsigned = FALSE;
1599 break;
1600 }
1601
1602 case MYSQL_TYPE_LONG:
1603 {
1604//d("LONG %d %s %d", i, fld->name, req->outBind[i].buffer_length);
1605 req->outBind[i].is_unsigned = FALSE;
1606 break;
1607 }
1608
1609 case MYSQL_TYPE_LONGLONG:
1610 {
1611//d("LONGLONG %s %d", fld->name, req->outBind[i].buffer_length);
1612 req->outBind[i].is_unsigned = FALSE;
1613 break;
1614 }
1615
1616 case MYSQL_TYPE_FLOAT:
1617 {
1618//d("FLOAT %s %d", fld->name, req->outBind[i].buffer_length);
1619 break;
1620 }
1621
1622 case MYSQL_TYPE_DOUBLE:
1623 {
1624//d("DOUBLE %s %d", fld->name, req->outBind[i].buffer_length);
1625 break;
1626 }
1627
1628 case MYSQL_TYPE_NEWDECIMAL:
1629 {
1630//d("NEWDECIMAL %s %d", fld->name, req->outBind[i].buffer_length);
1631 break;
1632 }
1633
1634 case MYSQL_TYPE_TIME:
1635 case MYSQL_TYPE_DATE:
1636 case MYSQL_TYPE_DATETIME:
1637 case MYSQL_TYPE_TIMESTAMP:
1638 {
1639//d("DATE / TIME ish %s %d", fld->name, req->outBind[i].buffer_length);
1640 break;
1641 }
1642
1643 case MYSQL_TYPE_STRING:
1644 case MYSQL_TYPE_VAR_STRING:
1645 {
1646//d("STRING / VARSTRING %s %d", fld->name, req->outBind[i].buffer_length);
1647 req->outBind[i].length = xzalloc(sizeof(unsigned long));
1648 break;
1649 }
1650
1651 case MYSQL_TYPE_TINY_BLOB:
1652 case MYSQL_TYPE_BLOB:
1653 case MYSQL_TYPE_MEDIUM_BLOB:
1654 case MYSQL_TYPE_LONG_BLOB:
1655 {
1656//d("BLOBs %s %d", fld->name, req->outBind[i].buffer_length);
1657 break;
1658 }
1659
1660 case MYSQL_TYPE_BIT:
1661 {
1662//d("BIT %s %d", fld->name, req->outBind[i].buffer_length);
1663 break;
1664 }
1665
1666 case MYSQL_TYPE_NULL:
1667 {
1668//d("NULL %s %d", fld->name, req->outBind[i].buffer_length);
1669 break;
1670 }
1671 }
1672 }
1673 }
1674 if (mysql_stmt_bind_result(req->prep, req->outBind))
1675 {
1676 E("Bind failed error %d.", mysql_stmt_errno(req->prep));
1677 ret++;
1678 goto freeIt;
1679 }
1680 }
1681 }
1682
1683
1684//d("input bind for %s", req->sql);
1685 for (i = 0; i < req->inCount; i++)
1686 {
1687 dbField *fld = req->fields->flds->get(req->fields->flds, req->inParams[i], NULL, false);
1688
1689 if (NULL == fld)
1690 {
1691 E("Unknown input field %s.%s for - %s", req->table, req->inParams[i], req->sql);
1692 ret++;
1693 goto freeIt;
1694 }
1695 else
1696 {
1697 switch(fld->type)
1698 {
1699 case MYSQL_TYPE_TINY:
1700 {
1701 int c = va_arg(ap, int);
1702 signed char d = (signed char) c;
1703
1704 memcpy(req->inBind[i].buffer, &d, (size_t) fld->length);
1705//T("TINY %d %s %d", i, fld->name, req->inBind[i].buffer_length);
1706 break;
1707 }
1708
1709 case MYSQL_TYPE_SHORT:
1710 {
1711 int c = va_arg(ap, int);
1712 short int d = (short int) c;
1713
1714 memcpy(req->inBind[i].buffer, &d, (size_t) fld->length);
1715//T("SHORT %d %s %d = %d", i, fld->name, req->inBind[i].buffer_length, c);
1716 break;
1717 }
1718
1719 case MYSQL_TYPE_INT24:
1720 {
1721 int d = va_arg(ap, int);
1722
1723 memcpy(req->inBind[i].buffer, &d, (size_t) fld->length);
1724//T("INT24 %d %s %d - %d", i, fld->name, req->inBind[i].buffer_length, d);
1725 break;
1726 }
1727
1728 case MYSQL_TYPE_LONG:
1729 {
1730 long d = va_arg(ap, long);
1731
1732 memcpy(req->inBind[i].buffer, &d, (size_t) fld->length);
1733//T("LONG %d %s %d = %ld", i, fld->name, req->inBind[i].buffer_length, d);
1734 break;
1735 }
1736
1737 case MYSQL_TYPE_LONGLONG:
1738 {
1739 long long int d = va_arg(ap, long long int);
1740
1741 memcpy(req->inBind[i].buffer, &d, (size_t) fld->length);
1742//T("LONGLONG %d %s %d = %lld", i, fld->name, req->inBind[i].buffer_length, d);
1743 break;
1744 }
1745
1746 case MYSQL_TYPE_FLOAT:
1747 {
1748 double c = va_arg(ap, double);
1749 float d = (float) c;
1750
1751 memcpy(req->inBind[i].buffer, &d, (size_t) fld->length);
1752//T("FLOAT %d %s %d = %f", i, fld->name, req->inBind[i].buffer_length, d);
1753 break;
1754 }
1755
1756 case MYSQL_TYPE_DOUBLE:
1757 {
1758 double d = va_arg(ap, double);
1759
1760 memcpy(req->inBind[i].buffer, &d, (size_t) fld->length);
1761//T("DOUBLE %d %s %d = %f", i, fld->name, req->inBind[i].buffer_length, d);
1762 break;
1763 }
1764
1765 case MYSQL_TYPE_NEWDECIMAL:
1766 {
1767//T("NEWDECIMAL %d %s %d", i, fld->name, req->inBind[i].buffer_length);
1768 break;
1769 }
1770
1771 case MYSQL_TYPE_TIME:
1772 case MYSQL_TYPE_DATE:
1773 case MYSQL_TYPE_DATETIME:
1774 case MYSQL_TYPE_TIMESTAMP:
1775 {
1776 MYSQL_TIME d = va_arg(ap, MYSQL_TIME);
1777
1778 memcpy(req->inBind[i].buffer, &d, (size_t) fld->length);
1779//T("DATE / TIME ish %d %s %d", i, fld->name, req->inBind[i].buffer_length);
1780 break;
1781 }
1782
1783 case MYSQL_TYPE_STRING:
1784 case MYSQL_TYPE_VAR_STRING:
1785 {
1786 char *d = va_arg(ap, char *);
1787 unsigned long l = strlen(d);
1788
1789 if (l > fld->length)
1790 l = fld->length;
1791 *(req->inBind[i].length) = l;
1792 strncpy(req->inBind[i].buffer, d, (size_t) l);
1793 ((char *) req->inBind[i].buffer)[l] = '\0';
1794//T("STRING / VARSTRING %d %s %d = %s", i, fld->name, req->inBind[i].buffer_length, d);
1795 break;
1796 }
1797
1798 case MYSQL_TYPE_TINY_BLOB:
1799 case MYSQL_TYPE_BLOB:
1800 case MYSQL_TYPE_MEDIUM_BLOB:
1801 case MYSQL_TYPE_LONG_BLOB:
1802 {
1803// TODO - should write this, we will likely need it. Main problem is - how long is this blob? Probably should add a length param before the blob.
1804//T("BLOBs %d %s %d", i, fld->name, req->inBind[i].buffer_length);
1805 break;
1806 }
1807
1808 case MYSQL_TYPE_BIT:
1809 {
1810//T("BIT %d %s %d", i, fld->name, req->inBind[i].buffer_length);
1811 break;
1812 }
1813
1814 case MYSQL_TYPE_NULL:
1815 {
1816//T("NULL %d %s %d", i, fld->name, req->inBind[i].buffer_length);
1817 break;
1818 }
1819 }
1820 }
1821 }
1822 if (mysql_stmt_bind_param(req->prep, req->inBind))
1823 {
1824 E("Bind failed error %d.", mysql_stmt_errno(req->prep));
1825 ret++;
1826 goto freeIt;
1827 }
1828
1829
1830//d("Execute %s", req->sql);
1831
1832 // do the prepared statement req->prep.
1833 if (mysql_stmt_execute(req->prep))
1834 {
1835 if (dbStmtCheckError(req, "Statement failed 0", req->sql))
1836 {
1837 ret++;
1838 goto freeIt;
1839 }
1840 }
1841
1842 int fs = mysql_stmt_field_count(req->prep);
1843 // stuff results back into req.
1844 if (NULL != req->outBind)
1845 {
1846 req->rows = xmalloc(sizeof(rowData));
1847 req->rows->fieldNames = xzalloc(fs * sizeof(char *));
1848 if (mysql_stmt_store_result(req->prep))
1849 {
1850 E(" mysql_stmt_store_result() failed %d: %s", mysql_stmt_errno(req->prep), mysql_stmt_error(req->prep));
1851 ret++;
1852 goto freeIt;
1853 }
1854 req->rowCount = mysql_stmt_num_rows(req->prep);
1855 if (0 == req->rowCount)
1856 D("No rows returned from : %s\n", req->sql);
1857 else
1858 D("%d rows of %d fields returned from : %s\n", req->rowCount, fs, req->sql);
1859
1860 req->rows->rows = qlist(0);
1861 while (MYSQL_NO_DATA != mysql_stmt_fetch(req->prep))
1862 {
1863 qhashtbl_t *flds = qhashtbl(0, 0);
1864
1865 for (i = 0; i < req->outCount; i++)
1866 {
1867 dbField *fld = req->fields->flds->get(req->fields->flds, req->outParams[i], NULL, false);
1868
1869 req->rows->fieldNames[i] = fld->name;
1870 if (!*(req->outBind[i].is_null))
1871 {
1872//d("2.8 %s", req->rows->fieldNames[i]);
1873 flds->put(flds, req->rows->fieldNames[i], req->outBind[i].buffer, req->outBind[i].buffer_length);
1874
1875 switch(fld->type)
1876 {
1877 case MYSQL_TYPE_TINY:
1878 {
1879 break;
1880 }
1881
1882 case MYSQL_TYPE_SHORT:
1883 {
1884 char *t = xmprintf("%d", (int) *((int *) req->outBind[i].buffer));
1885 flds->putstr(flds, req->rows->fieldNames[i], t);
1886 free(t);
1887 break;
1888 }
1889
1890 case MYSQL_TYPE_INT24:
1891 {
1892 char *t = xmprintf("%d", (int) *((int *) req->outBind[i].buffer));
1893 flds->putstr(flds, req->rows->fieldNames[i], t);
1894 free(t);
1895 break;
1896 }
1897
1898 case MYSQL_TYPE_LONG:
1899 {
1900 if (NULL == req->outBind[i].buffer)
1901 {
1902 E("Field %d %s is NULL", i, fld->name);
1903 ret++;
1904 goto freeIt;
1905 }
1906 char *t = xmprintf("%d", (int) *((int *) (req->outBind[i].buffer)));
1907//d("Setting %i %s %s", i, fld->name, t);
1908 flds->putstr(flds, req->rows->fieldNames[i], t);
1909 free(t);
1910 break;
1911 }
1912
1913 case MYSQL_TYPE_LONGLONG:
1914 {
1915 char *t = xmprintf("%d", (int) *((int *) req->outBind[i].buffer));
1916 flds->putstr(flds, req->rows->fieldNames[i], t);
1917 free(t);
1918 break;
1919 }
1920
1921 case MYSQL_TYPE_FLOAT:
1922 {
1923 break;
1924 }
1925
1926 case MYSQL_TYPE_DOUBLE:
1927 {
1928 break;
1929 }
1930
1931 case MYSQL_TYPE_NEWDECIMAL:
1932 {
1933 break;
1934 }
1935
1936 case MYSQL_TYPE_TIME:
1937 case MYSQL_TYPE_DATE:
1938 case MYSQL_TYPE_DATETIME:
1939 case MYSQL_TYPE_TIMESTAMP:
1940 {
1941 break;
1942 }
1943
1944 case MYSQL_TYPE_STRING:
1945 case MYSQL_TYPE_VAR_STRING:
1946 {
1947 break;
1948 }
1949
1950 case MYSQL_TYPE_TINY_BLOB:
1951 case MYSQL_TYPE_BLOB:
1952 case MYSQL_TYPE_MEDIUM_BLOB:
1953 case MYSQL_TYPE_LONG_BLOB:
1954 {
1955 break;
1956 }
1957
1958 case MYSQL_TYPE_BIT:
1959 {
1960 break;
1961 }
1962
1963 case MYSQL_TYPE_NULL:
1964 {
1965 break;
1966 }
1967 }
1968 }
1969 else
1970 D("Not setting data %s, coz it's NULL", fld->name);
1971 }
1972 req->rows->rows->addlast(req->rows->rows, flds, sizeof(qhashtbl_t));
1973 free(flds);
1974 }
1975 }
1976
1977freeIt:
1978 if (prepare_meta_result)
1979 mysql_free_result(prepare_meta_result);
1980 if (mysql_stmt_free_result(req->prep))
1981 {
1982 E("Statement result freeing failed %d: %s\n", mysql_stmt_errno(req->prep), mysql_stmt_error(req->prep));
1983 ret++;
1984 }
1985
1986end:
1987 va_end(ap);
1988
1989 if (-1 == clock_gettime(CLOCK_REALTIME, &dbLast))
1990 perror_msg("Unable to get the time.");
1991 n = (dbLast.tv_sec * 1000000000.0) + dbLast.tv_nsec;
1992 T("dbDoSomething(%s) took %lf seconds", req->sql, (n - t) / 1000000000.0);
1993
1994 return ret;
1995}
1996
1997// Copy the SQL results into the request structure.
1998void dbPull(reqData *Rd, char *table, rowData *rows)
1999{
2000 char *where;
2001 qhashtbl_t *me = rows->rows->popfirst(rows->rows, NULL);
2002 qhashtbl_obj_t obj;
2003
2004 if (NULL != me)
2005 {
2006 memset((void*)&obj, 0, sizeof(obj));
2007 me->lock(me);
2008 while(me->getnext(me, &obj, false) == true)
2009 {
2010 where = xmprintf("%s.%s", table, obj.name);
2011d("dbPull(Rd->database) %s = %s", where, (char *) obj.data);
2012 Rd->database->putstr(Rd->database, where, (char *) obj.data);
2013 free(where);
2014 }
2015 me->unlock(me);
2016 me->free(me);
2017 }
2018 free(rows->fieldNames);
2019 rows->rows->free(rows->rows);
2020 free(rows);
2021}
2022
2023my_ulonglong dbCount(char *table, char *where)
2024{
2025 my_ulonglong ret = 0;
2026 char *sql;
2027 struct timespec now, then;
2028
2029 if (-1 == clock_gettime(CLOCK_REALTIME, &then))
2030 perror_msg("Unable to get the time.");
2031
2032 if (where)
2033 sql = xmprintf("SELECT Count(*) FROM %s WHERE %s", table, where);
2034 else
2035 sql = xmprintf("SELECT Count(*) FROM %s", table);
2036
2037 if (mysql_query(database, sql))
2038 {
2039// E("MariaDB error %d - Query failed 1: %s", mysql_errno(database), mysql_error(database));
2040 if (dbCheckError("Query failed 1", sql))
2041 {
2042 ret = dbCount(table, where);
2043 free(sql);
2044 return ret;
2045 }
2046 }
2047 else
2048 {
2049 MYSQL_RES *result = mysql_store_result(database);
2050
2051 if (!result)
2052 E("Couldn't get results set from %s\n: %s", sql, mysql_error(database));
2053 else
2054 {
2055 MYSQL_ROW row = mysql_fetch_row(result);
2056 if (!row)
2057 E("MariaDB error %d - Couldn't get row from %s\n: %s", mysql_errno(database), sql, mysql_error(database));
2058 else
2059 ret = atoll(row[0]);
2060 mysql_free_result(result);
2061 }
2062 }
2063
2064 if (-1 == clock_gettime(CLOCK_REALTIME, &now))
2065 perror_msg("Unable to get the time.");
2066 double n = (now.tv_sec * 1000000000.0) + now.tv_nsec;
2067 double t = (then.tv_sec * 1000000000.0) + then.tv_nsec;
2068// T("dbCount(%s) took %lf seconds", sql, (n - t) / 1000000000.0);
2069 free(sql);
2070 return ret;
2071}
2072
2073my_ulonglong dbCountJoin(char *table, char *select, char *join, char *where)
2074{
2075 my_ulonglong ret = 0;
2076 char *sql;
2077 struct timespec now, then;
2078
2079 if (-1 == clock_gettime(CLOCK_REALTIME, &then))
2080 perror_msg("Unable to get the time.");
2081
2082 if (NULL == select)
2083 select = "*";
2084 if (NULL == join)
2085 join = "";
2086
2087 if (where)
2088 sql = xmprintf("SELECT %s FROM %s %s WHERE %s", select, table, join, where);
2089 else
2090 sql = xmprintf("SELECT %s FROM %s", select, table, join);
2091
2092 if (mysql_query(database, sql))
2093 {
2094// E("MariaDB error %d - Query failed 2: %s", mysql_errno(database), mysql_error(database));
2095 if (dbCheckError("Query failed 2", sql))
2096 {
2097 ret = dbCountJoin(table, select, join, where);
2098 free(sql);
2099 return ret;
2100 }
2101 }
2102 else
2103 {
2104 MYSQL_RES *result = mysql_store_result(database);
2105
2106 if (!result)
2107 E("MariaDB error %d - Couldn't get results set from %s\n: %s", mysql_errno(database), sql, mysql_error(database));
2108 else
2109 ret = mysql_num_rows(result);
2110 mysql_free_result(result);
2111 }
2112
2113 if (-1 == clock_gettime(CLOCK_REALTIME, &now))
2114 perror_msg("Unable to get the time.");
2115 double n = (now.tv_sec * 1000000000.0) + now.tv_nsec;
2116 double t = (then.tv_sec * 1000000000.0) + then.tv_nsec;
2117// T("dbCointJoin(%s) took %lf seconds", sql, (n - t) / 1000000000.0);
2118 free(sql);
2119 return ret;
2120}
2121
2122
2123void replaceStr(qhashtbl_t *ssi, char *key, char *value)
2124{
2125 ssi->putstr(ssi, key, value);
2126}
2127
2128void replaceLong(qhashtbl_t *ssi, char *key, my_ulonglong value)
2129{
2130 char *tmp = xmprintf("%lu", value);
2131
2132 replaceStr(ssi, key, tmp);
2133 free(tmp);
2134}
2135
2136
2137float timeDiff(struct timeval *now, struct timeval *then)
2138{
2139 if (0 == gettimeofday(now, NULL))
2140 {
2141 struct timeval thisTime = { 0, 0 };
2142 double result = 0.0;
2143
2144 thisTime.tv_sec = now->tv_sec;
2145 thisTime.tv_usec = now->tv_usec;
2146 if (thisTime.tv_usec < then->tv_usec)
2147 {
2148 thisTime.tv_sec--;
2149 thisTime.tv_usec += 1000000;
2150 }
2151 thisTime.tv_usec -= then->tv_usec;
2152 thisTime.tv_sec -= then->tv_sec;
2153 result = ((double) thisTime.tv_usec) / ((double) 1000000.0);
2154 result += thisTime.tv_sec;
2155 return result;
2156 }
2157
2158 return 0.0;
2159}
2160
2161
2162gridStats *getStats(MYSQL *db, gridStats *stats)
2163{
2164 if (NULL == stats)
2165 {
2166 stats = xmalloc(sizeof(gridStats));
2167 stats->next = 30;
2168 gettimeofday(&(stats->last), NULL);
2169 stats->stats = qhashtbl(0, 0);
2170 stats->stats->putstr(stats->stats, "version", "SledjChisl FCGI Dev 0.1");
2171 stats->stats->putstr(stats->stats, "grid", "my grid");
2172 stats->stats->putstr(stats->stats, "uri", "http://localhost:8002/");
2173 if (checkSimIsRunning("ROBUST"))
2174 stats->stats->putstr(stats->stats, "gridOnline", "online");
2175 else
2176 stats->stats->putstr(stats->stats, "gridOnline", "offline");
2177 }
2178 else
2179 {
2180 static struct timeval thisTime;
2181 if (stats->next > timeDiff(&thisTime, &(stats->last)))
2182 return stats;
2183 }
2184
2185 I("Getting fresh grid stats.");
2186 if (checkSimIsRunning("ROBUST"))
2187 replaceStr(stats->stats, "gridOnline", "online");
2188 else
2189 replaceStr(stats->stats, "gridOnline", "offline");
2190
2191 char *tmp;
2192 my_ulonglong locIn = dbCount("Presence", "RegionID != '00000000-0000-0000-0000-000000000000'"); // Locals online but not HGing, and HGers in world.
2193 my_ulonglong HGin = dbCount("Presence", "UserID NOT IN (SELECT PrincipalID FROM UserAccounts)"); // HGers in world.
2194
2195 // Collect stats about members.
2196 replaceLong(stats->stats, "hgers", HGin);
2197 if (locIn >= HGin) // Does OpenSim have too many ghosts?
2198 replaceLong(stats->stats, "inworld", locIn - HGin);
2199 else
2200 replaceLong(stats->stats, "inworld", 0);
2201 tmp = xmprintf("GridExternalName != '%s'", stats->stats->getstr(stats->stats, "uri", false));
2202 replaceLong(stats->stats, "outworld", dbCount("hg_traveling_data", tmp));
2203 free(tmp);
2204 replaceLong(stats->stats, "members", dbCount("UserAccounts", NULL));
2205
2206 // Count local and HG visitors for the last 30 and 60 days.
2207 locIn = dbCountJoin("GridUser", "GridUser.UserID", "INNER JOIN UserAccounts ON GridUser.UserID = UserAccounts.PrincipalID",
2208 "Login > UNIX_TIMESTAMP(FROM_UNIXTIME(UNIX_TIMESTAMP(now()) - 2419200))");
2209 HGin = dbCount("GridUser", "Login > UNIX_TIMESTAMP(FROM_UNIXTIME(UNIX_TIMESTAMP(now()) - 2419200))");
2210 replaceLong(stats->stats, "locDay30", locIn);
2211 replaceLong(stats->stats, "day30", HGin);
2212 replaceLong(stats->stats, "HGday30", HGin - locIn);
2213
2214 locIn = dbCountJoin("GridUser", "GridUser.UserID", "INNER JOIN UserAccounts ON GridUser.UserID = UserAccounts.PrincipalID",
2215 "Login > UNIX_TIMESTAMP(FROM_UNIXTIME(UNIX_TIMESTAMP(now()) - 4838400))");
2216 HGin = dbCount("GridUser", "Login > UNIX_TIMESTAMP(FROM_UNIXTIME(UNIX_TIMESTAMP(now()) - 4838400))");
2217 replaceLong(stats->stats, "locDay60", locIn);
2218 replaceLong(stats->stats, "day60", HGin);
2219 replaceLong(stats->stats, "HGday60", HGin - locIn);
2220
2221 // Collect stats about sims.
2222 replaceLong(stats->stats, "sims", dbCount("regions", NULL));
2223 replaceLong(stats->stats, "onlineSims", dbCount("regions", "sizeX != 0"));
2224 replaceLong(stats->stats, "varRegions", dbCount("regions", "sizeX > 256 or sizeY > 256"));
2225 replaceLong(stats->stats, "singleSims", dbCount("regions", "sizeX = 256 and sizeY = 256"));
2226 replaceLong(stats->stats, "offlineSims", dbCount("regions", "sizeX = 0"));
2227
2228 // Calculate total size of all regions.
2229 my_ulonglong simSize = 0;
2230 static dbRequest *rgnSizes = NULL;
2231 if (NULL == rgnSizes)
2232 {
2233 static char *szi[] = {NULL};
2234 static char *szo[] = {"sizeX", "sizeY", NULL};
2235 rgnSizes = xzalloc(sizeof(dbRequest));
2236 rgnSizes->table = "regions";
2237 rgnSizes->inParams = szi;
2238 rgnSizes->outParams = szo;
2239 rgnSizes->where = "sizeX != 0";
2240 dbRequests->addfirst(dbRequests, &rgnSizes, sizeof(dbRequest *));
2241 }
2242 dbDoSomething(rgnSizes, FALSE); // LEAKY
2243 rowData *rows = rgnSizes->rows;
2244
2245 qhashtbl_t *row;
2246 while (NULL != (row = rows->rows->getat(rows->rows, 0, NULL, true)))
2247 {
2248 my_ulonglong x = 0, y = 0;
2249
2250 tmp = row->getstr(row, "sizeX", false);
2251 if (NULL == tmp)
2252 E("No regions.sizeX!");
2253 else
2254 x = atoll(tmp);
2255 tmp = row->getstr(row, "sizeY", false);
2256 if (NULL == tmp)
2257 E("No regions.sizeY!");
2258 else
2259 y = atoll(tmp);
2260 simSize += x * y;
2261 row->free(row);
2262 rows->rows->removefirst(rows->rows);
2263 }
2264 free(rows->fieldNames);
2265 rows->rows->free(rows->rows);
2266 free(rows);
2267
2268 tmp = xmprintf("%lu", simSize);
2269 stats->stats->putstr(stats->stats, "simsSize", tmp);
2270 free(tmp);
2271 gettimeofday(&(stats->last), NULL);
2272
2273 return stats;
2274}
2275
2276
2277qhashtbl_t *toknize(char *text, char *delims)
2278{
2279 qhashtbl_t *ret = qhashtbl(0, 0);
2280
2281 if (NULL == text)
2282 return ret;
2283
2284 char *txt = xstrdup(text), *token, dlm = ' ', *key, *val = NULL;
2285 int offset = 0;
2286
2287 while((token = qstrtok(txt, delims, &dlm, &offset)) != NULL)
2288 {
2289 if (delims[0] == dlm)
2290 {
2291 key = token;
2292 val = &txt[offset];
2293 }
2294 else if (delims[1] == dlm)
2295 {
2296 ret->putstr(ret, qstrtrim_head(key), token);
2297d(" %s = %s", qstrtrim_head(key), val);
2298 val = NULL;
2299 }
2300 }
2301 if (NULL != val)
2302{
2303 ret->putstr(ret, qstrtrim_head(key), val);
2304d(" %s = %s", qstrtrim_head(key), val);
2305}
2306 free(txt);
2307 return ret;
2308}
2309
2310void santize(qhashtbl_t *tbl)
2311{
2312 qhashtbl_obj_t obj;
2313
2314 memset((void*)&obj, 0, sizeof(obj));
2315 tbl->lock(tbl);
2316 while(tbl->getnext(tbl, &obj, false) == true)
2317 {
2318 char *n = obj.name, *o = (char *) obj.data;
2319
2320 qurl_decode(o);
2321 tbl->putstr(tbl, n, o);
2322 }
2323 tbl->unlock(tbl);
2324}
2325
2326void outize(qgrow_t *reply, qhashtbl_t *tbl, char *label)
2327{
2328 reply->addstrf(reply, "%s:<br>\n<pre>\n", label);
2329 qhashtbl_obj_t obj;
2330 memset((void*)&obj, 0, sizeof(obj));
2331 tbl->lock(tbl);
2332 while(tbl->getnext(tbl, &obj, false) == true)
2333 reply->addstrf(reply, " &nbsp; %s = %s\n", obj.name, (char *) obj.data);
2334 tbl->unlock(tbl);
2335 reply->addstr(reply, "</pre>\n");
2336}
2337
2338// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie
2339enum cookieSame
2340{
2341 CS_NOT,
2342 CS_STRICT,
2343 CS_LAX, // Apparently the default set by browsers these days.
2344 CS_NONE
2345};
2346typedef struct _cookie cookie;
2347struct _cookie
2348{
2349 char *value, *domain, *path;
2350 // char *expires; // Use maxAge instead, it's far simpler to figure out.
2351 int maxAge;
2352 boolean secure, httpOnly;
2353 enum cookieSame site;
2354};
2355
2356void freeCookie(reqData *Rd, char *cki)
2357{
2358 cookie *ck0 = Rd->Rcookies->get(Rd->Rcookies, cki, NULL, false);
2359
2360 if (NULL != ck0)
2361 {
2362 if (NULL != ck0->value)
2363 free(ck0->value);
2364 Rd->Rcookies->remove(Rd->Rcookies, cki);
2365 }
2366}
2367
2368cookie *setCookie(reqData *Rd, char *cki, char *value)
2369{
2370 cookie *ret = xzalloc(sizeof(cookie));
2371 char *cook = xstrdup(cki);
2372 int l, i;
2373
2374// TODO - would URL encoding do the trick?
2375 // Validate this, as there is a limited set of characters allowed.
2376 qstrreplace("tr", cook, "()<>@,;:\\\"/[]?={} \t", "_");
2377 freeCookie(Rd, cook);
2378 l = strlen(cook);
2379 for (i = 0; i < l; i++)
2380 {
2381 if (iscntrl(cook[i]) != 0)
2382 cook[i] = '_';
2383 }
2384 l = strlen(value);
2385 if (0 != l)
2386 ret->value = qurl_encode(value, l);
2387 else
2388 ret->value = xstrdup("");
2389 ret->httpOnly = TRUE;
2390 ret->site = CS_STRICT;
2391 ret->secure = TRUE;
2392 ret->path = getStrH(Rd->headers, "SCRIPT_NAME");
2393 Rd->Rcookies->put(Rd->Rcookies, cook, ret, sizeof(cookie));
2394 free(ret);
2395 ret = Rd->Rcookies->get(Rd->Rcookies, cook, NULL, false);
2396 free(cook);
2397 return ret;
2398}
2399
2400char *getCookie(qhashtbl_t *cookies, char *cki)
2401{
2402 char *ret = NULL;
2403 cookie *ck = (cookie *) cookies->get(cookies, cki, NULL, false);
2404
2405 if (NULL != ck)
2406 ret = ck->value;
2407 return ret;
2408}
2409
2410void outizeCookie(qgrow_t *reply, qhashtbl_t *tbl, char *label)
2411{
2412 reply->addstrf(reply, "%s:<br>\n<pre>\n", label);
2413 qhashtbl_obj_t obj;
2414 memset((void*)&obj, 0, sizeof(obj));
2415 tbl->lock(tbl);
2416 while(tbl->getnext(tbl, &obj, false) == true)
2417 reply->addstrf(reply, " &nbsp; %s = %s\n", obj.name, ((cookie *) obj.data)->value);
2418 tbl->unlock(tbl);
2419 reply->addstr(reply, "</pre>\n");
2420}
2421
2422void list2cookie(reqData *Rd, char *cki, qlist_t *list)
2423{
2424 char *t0 = xstrdup("");
2425 qlist_obj_t obj;
2426
2427 memset((void*)&obj, 0, sizeof(obj)); // must be cleared before call
2428 list->lock(list);
2429 while (list->getnext(list, &obj, false) == true)
2430 {
2431 char *t1 = xmprintf("%s\n%s", t0, (char *) obj.data);
2432
2433 free(t0);
2434 t0 = t1;
2435 }
2436 list->unlock(list);
2437 setCookie(Rd, cki, &t0[1]); // Skip the first empty one.
2438// TODO - should set a very short maxAge.
2439 free(t0);
2440}
2441
2442qlist_t *cookie2list(qhashtbl_t *cookies, char *cki)
2443{
2444 qlist_t *ret = NULL;
2445 cookie *ck = (cookie *) cookies->get(cookies, cki, NULL, false);
2446
2447 if (NULL != ck)
2448 {
2449 if (NULL != ck->value)
2450 {
2451 qurl_decode(ck->value);
2452 ret = qstrtokenizer(ck->value, "\n");
2453 free(ck->value);
2454 }
2455// TODO - should send the "delete this cookie" thing to the browser.
2456 cookies->remove(cookies, cki);
2457 }
2458 return ret;
2459}
2460
2461
2462enum fragmentType
2463{
2464 FT_TEXT,
2465 FT_PARAM,
2466 FT_LUA
2467};
2468
2469typedef struct _fragment fragment;
2470struct _fragment
2471{
2472 enum fragmentType type;
2473 int length;
2474 char *text;
2475};
2476
2477static void HTMLdebug(qgrow_t *reply)
2478{
2479 reply->addstrf(reply,
2480 " <p class='hoverItem'>\n"
2481 " <div class='hoverWrapper0'>\n"
2482 " <p>DEBUG</p>\n"
2483 " <div id='hoverShow0'>\n"
2484 " <h1>DEBUG log</h1>\n"
2485 " <!--#echo var=\"DEBUG\" -->\n"
2486 " </div>\n"
2487 " </div>\n"
2488 " </p>\n"
2489 );
2490}
2491
2492static void HTMLheader(qgrow_t *reply, char *title)
2493{
2494 reply->addstrf(reply,
2495 "<html>\n"
2496 " <head>\n"
2497 " <title>%s</title>\n"
2498 " <meta charset=\"UTF-8\">\n"
2499 " <link rel=\"shortcut icon\" href=\"/SledjHamrIconSmall.png\">\n"
2500 , title);
2501 reply->addstrf(reply, " <link type='text/css' rel='stylesheet' href='/SledjChisl.css' media='all' />\n");
2502
2503 if (DEBUG)
2504 reply->addstrf(reply, " <link type='text/css' rel='stylesheet' href='/debugStyle.css' media='all' />\n");
2505
2506 reply->addstrf(reply,
2507 " <style> \n"
2508 " html, body {background-color: black; color: white; font-family: 'sans-serif'; margin: 0; padding: 0;}\n"
2509 " a:link {color: aqua;}\n"
2510 " a:visited {color: fuchsia;}\n"
2511 " a:hover {color: blue;}\n"
2512 " a:active {color: red;}\n"
2513 " button {background-color: darkgreen; color: white; font-family: 'sans-serif';}\n"
2514 " button:hover {color: blue;}\n"
2515 " button:active {color: red;}\n"
2516 " label {background-color:darkgreen; color: white; font-family: 'sans-serif'; font-size: 160%;}\n"
2517 " input {background-color:darkblue; color: white; font-family: 'sans-serif'; font-size: 80%;}\n"
2518 // What idiot thought aligning the label with the bottom of textareas was a good default?
2519 " textarea {background-color:darkblue; color: white; font-family: 'sans-serif'; font-size: 80%; vertical-align: top;}\n"
2520 " </style>\n"
2521 " </head>\n"
2522 " <body bgcolor='black' text='white' link='aqua' vlink='fuchsia' alink='red'>\n"
2523 " <font face='sans-serif'>\n"
2524 );
2525 reply->addstrf(reply, " <div class='top-left'>\n");
2526 if (DEBUG)
2527 HTMLdebug(reply);
2528}
2529
2530// TODO - maybe escape non printables as well?
2531char *HTMLentities[] =
2532{
2533 "", "", "", "", "", "", "", "", "", // NUL SOH STX ETX EOT ENQ ACK BEL BS
2534 "&Tab;", "&NewLine;",
2535 "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", // VT FF CR SO SI DLE DC1 DC2 DC3 DC4 NAK SYN ETB CAN EM SUB ESC FS GS RS US
2536 " ", // Space
2537 "&excl;", "&quot;",
2538 "&num;", "&dollar;",
2539 "&percnt;", "&amp;",
2540 "&apos;",
2541 "&lpar;", "&rpar;",
2542 "&ast;",
2543 "&plus;",
2544 "&comma;",
2545 "-",
2546 "&period;",
2547 "&sol;",
2548 "0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
2549 "&colon;", "&semi;",
2550 "&lt;", "&equals;", "&gt;",
2551 "&quest;",
2552 "&commat;",
2553 "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z",
2554 "&lsqb;", "&bsol;", "&rsqb;",
2555 "&Hat;",
2556 "&lowbar;",
2557 "&grave;",
2558 "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z",
2559 "&lcub;", "&verbar;", "&rcub;",
2560 "~",
2561 "", // DEL
2562 "&nbsp;" // This is actually 160, not 128, but I hack around that.
2563};
2564static void HTMLescapeString(qgrow_t *reply, char *string)
2565{
2566 size_t l = strlen(string);
2567 char *t = xmalloc(l * 10 + 1);
2568 int i, j = 0;
2569 boolean space = FALSE;
2570
2571 for (i = 0; i < l; i++)
2572 {
2573 int s = string[i];
2574
2575 // Alternate long line of spaces with space and &nbsp;.
2576 if (' ' == s)
2577 {
2578 if (space)
2579 {
2580 s = 128;
2581 space = FALSE;
2582 }
2583 else
2584 space = TRUE;
2585 }
2586 else
2587 {
2588 space = FALSE;
2589 if (128 == s) // The real 128 character.
2590 {
2591 t[j++] = ' ';
2592 continue;
2593 }
2594 }
2595
2596 if (128 >= s)
2597 {
2598 char *r = HTMLentities[s];
2599 size_t m = strlen(r);
2600 int k;
2601
2602 for (k = 0; k < m; k++)
2603 t[j++] = r[k];
2604 }
2605 else
2606 t[j++] = ' ';
2607 }
2608 t[j] = '\0';
2609 reply->addstr(reply, t);
2610 free(t);
2611}
2612
2613static void HTMLtable(qgrow_t *reply, MYSQL *db, MYSQL_RES *result, char *caption, char *URL, char *id)
2614{
2615 char *tbl = "";
2616 char *address, *addrend, *t, *t0;
2617 int count = 0, c = -1, i;
2618 MYSQL_ROW row;
2619 MYSQL_FIELD *fields = mysql_fetch_fields(result);
2620
2621 reply->addstrf(reply, "<table border=\"1\"><caption>%s</caption>\n", caption);
2622
2623 if (!fields)
2624 E("Failed fetching fields: %s", mysql_error(db));
2625 while ((row = mysql_fetch_row(result)))
2626 {
2627 reply->addstr(reply, "<tr>");
2628 address = xmprintf("");
2629 addrend = "";
2630
2631 if (-1 == c)
2632 c = mysql_num_fields(result);
2633
2634 if (0 == count)
2635 {
2636 for (i = 0; i < c; i++)
2637 {
2638 char *s = fields[i].name;
2639
2640 reply->addstrf(reply, "<th>%s</th>", s);
2641 }
2642 reply->addstr(reply, "</tr>\n<tr>");
2643 }
2644
2645 if (NULL != URL)
2646 {
2647 free(address);
2648 address = xmprintf("<a href=\"%s", URL);
2649 addrend = "</a>";
2650 }
2651
2652 for (i = 0; i < c; i++)
2653 {
2654 char *s = fields[i].name;
2655
2656 t0 = row[i];
2657 if (NULL == t0)
2658 E("No field %s!", s);
2659 else
2660 {
2661 if ((NULL != id) && (strcmp(s, id) == 0))
2662 reply->addstrf(reply, "<td>%s&%s=%s\">%s%s</td>", address, id, t0, t0, addrend);
2663 else
2664 reply->addstrf(reply, "<td>%s</td>", t0);
2665 }
2666 }
2667 reply->addstr(reply, "</tr>\n");
2668
2669 free(address);
2670 count++;
2671 }
2672
2673 reply->addstr(reply, "</table>");
2674 mysql_free_result(result);
2675}
2676
2677static void HTMLhidden(qgrow_t *reply, char *name, char *val)
2678{
2679 if ((NULL != val) && ("" != val))
2680 {
2681 reply->addstrf(reply, " <input type=\"hidden\" name=\"%s\" value=\"", name);
2682 HTMLescapeString(reply, val);
2683 reply->addstr(reply, "\">\n");
2684 }
2685}
2686
2687static void HTMLform(qgrow_t *reply, char *action, char *token)
2688{
2689 reply->addstrf(reply, " <form action=\"%s\" method=\"POST\">\n", action);
2690 if ((NULL != token) && ('\0' != token[0]))
2691 HTMLhidden(reply, "munchie", token);
2692}
2693static void HTMLformEnd(qgrow_t *reply)
2694{
2695 reply->addstrf(reply, " </form>\n");
2696}
2697
2698static void HTMLcheckBox(qgrow_t *reply, char *name, char *title, boolean checked, boolean required)
2699{
2700 // HTML is an absolute fucking horror. This is so that we got an off sent to us if the checkbox is off, otherwise we get nothing.
2701 HTMLhidden(reply, name, "off");
2702 reply->addstrf(reply, " <p>");
2703 reply->addstrf(reply, "<label><input type=\"checkbox\" name=\"%s\"", name);
2704 if (checked)
2705 reply->addstrf(reply, " checked", name);
2706// if (required)
2707// reply->addstr(reply, " required");
2708 reply->addstrf(reply, "> %s </label>", title);
2709// reply->addstrf(reply, "> %s ⛝ □ 🞐 🞎 🞎 ☐ ▣️ ◉ ○ </label>", title);
2710 reply->addstrf(reply, "</p>\n");
2711}
2712
2713static 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)
2714{
2715 reply->addstrf(reply, " <p><label>%s : <textarea name=\"%s\"", title, name);
2716 if (0 < rows)
2717 reply->addstrf(reply, " rows=\"%d\"", rows);
2718 if (0 < cols)
2719 reply->addstrf(reply, " cols=\"%d\"", cols);
2720 if (0 < min)
2721 reply->addstrf(reply, " minlength=\"%d\"", min);
2722 if (0 < max)
2723 reply->addstrf(reply, " maxlength=\"%d\"", max);
2724 if (required)
2725 reply->addstr(reply, " required");
2726 if (readOnly)
2727 reply->addstr(reply, " readonly");
2728 if ("" != holder)
2729 reply->addstrf(reply, " placeholder=\"%s\"", holder);
2730 if ("" != comp)
2731 reply->addstrf(reply, " autocomplete=\"%s\"", comp);
2732 if ("" != spell)
2733 reply->addstrf(reply, " spellcheck=\"%s\"", spell);
2734 if ("" != wrap)
2735 reply->addstrf(reply, " wrap=\"%s\"", wrap);
2736 if ((NULL != val) && ("" != val))
2737 {
2738 reply->addstr(reply, ">");
2739 HTMLescapeString(reply, val);
2740 reply->addstr(reply, "</textarea></label></p>\n");
2741 }
2742 else
2743 reply->addstrf(reply, "></textarea></label></p>\n");
2744}
2745
2746static void HTMLtext(qgrow_t *reply, char *type, char *title, char *name, char *val, int size, int max, boolean required)
2747{
2748 reply->addstrf(reply, " <p><label>%s : <input type=\"%s\" name=\"%s\"", title, type, name);
2749 if ((NULL != val) && ("" != val))
2750 {
2751 reply->addstr(reply, " value=\"");
2752 HTMLescapeString(reply, val);
2753 reply->addstr(reply, "\"");
2754 }
2755 if (0 < size)
2756 reply->addstrf(reply, " size=\"%d\"", size);
2757 if (0 < max)
2758 reply->addstrf(reply, " maxlength=\"%d\"", max);
2759 if (required)
2760 reply->addstr(reply, " required");
2761 reply->addstr(reply, "></label></p>\n");
2762}
2763
2764static void HTMLselect(qgrow_t *reply, char *title, char *name)
2765{
2766 if (NULL == title)
2767 reply->addstrf(reply, " <p><select name=\"%s\">", name);
2768 else
2769 reply->addstrf(reply, " <p><label>%s : \n <select name=\"%s\">\n", title, name);
2770}
2771static void HTMLselectEnd(qgrow_t *reply)
2772{
2773 reply->addstr(reply, " </select></label></p>\n \n");
2774}
2775static void HTMLselectEndNo(qgrow_t *reply)
2776{
2777 reply->addstr(reply, " </select></p>");
2778}
2779
2780static void HTMLoption(qgrow_t *reply, char *title, boolean selected)
2781{
2782 char *sel = "";
2783
2784 if (selected)
2785 sel = " selected";
2786 reply->addstrf(reply, " <option value=\"%s\"%s>%s</option>\n", title, sel, title);
2787}
2788
2789static void HTMLbutton(qgrow_t *reply, char *name, char *title)
2790{
2791 reply->addstrf(reply, " <button type=\"submit\" name=\"doit\" value=\"%s\">%s</button>\n", name, title);
2792}
2793
2794static void HTMLlist(qgrow_t *reply, char *title, qlist_t *list)
2795{
2796 qlist_obj_t obj;
2797
2798 reply->addstrf(reply, "<ul>%s\n", title);
2799 memset((void*)&obj, 0, sizeof(obj)); // must be cleared before call
2800 list->lock(list);
2801 while (list->getnext(list, &obj, false) == true)
2802 {
2803 reply->addstr(reply, "<li>");
2804 HTMLescapeString(reply, (char *) obj.data);
2805 reply->addstr(reply, "</li>\n");
2806 }
2807 list->unlock(list);
2808 reply->addstr(reply, "</ul>\n");
2809}
2810
2811static int count = 0;
2812void HTMLfill(reqData *Rd, enum fragmentType type, char *text, int length)
2813{
2814 char *tmp;
2815
2816 switch (type)
2817 {
2818 case FT_TEXT:
2819 {
2820 if (length)
2821 Rd->reply->add(Rd->reply, (void *) text, length * sizeof(char));
2822 break;
2823 }
2824
2825 case FT_PARAM:
2826 {
2827 if (strcmp("DEBUG", text) == 0)
2828 {
2829 if (DEBUG)
2830 {
2831 Rd->reply->addstrf(Rd->reply, "<h1>FastCGI SledjChisl</h1>\n"
2832 "<p>Request number %d, Process ID: %d</p>\n", count++, getpid());
2833 Rd->reply->addstrf(Rd->reply, "<p>libfcgi version: %s</p>\n", FCGI_VERSION);
2834 Rd->reply->addstrf(Rd->reply, "<p>Lua version: %s</p>\n", LUA_RELEASE);
2835 Rd->reply->addstrf(Rd->reply, "<p>LuaJIT version: %s</p>\n", LUAJIT_VERSION);
2836 Rd->reply->addstrf(Rd->reply, "<p>MySQL client version: %s</p>\n", mysql_get_client_info());
2837 outize(Rd->reply, Rd->headers, "Environment");
2838 outize(Rd->reply, Rd->cookies, "Cookies");
2839 outize(Rd->reply, Rd->queries, "Query");
2840 outize(Rd->reply, Rd->body, "POST body");
2841 outize(Rd->reply, Rd->stuff, "Stuff");
2842 showSesh(Rd->reply, &Rd->shs);
2843 if (Rd->lnk) showSesh(Rd->reply, Rd->lnk);
2844 outize(Rd->reply, Rd->database, "Database");
2845 outizeCookie(Rd->reply, Rd->Rcookies, "Reply Cookies");
2846 outize(Rd->reply, Rd->Rheaders, "Reply HEADERS");
2847 }
2848 }
2849 else if (strcmp("URL", text) == 0)
2850 Rd->reply->addstrf(Rd->reply, "%s://%s%s", Rd->Scheme, Rd->Host, Rd->Script);
2851 else
2852 {
2853 if ((tmp = Rd->stats->stats->getstr(Rd->stats->stats, text, false)) != NULL)
2854 Rd->reply->addstr(Rd->reply, tmp);
2855 else
2856 Rd->reply->addstrf(Rd->reply, "<b>%s</b>", text);
2857 }
2858 break;
2859 }
2860
2861 case FT_LUA:
2862 break;
2863 }
2864}
2865
2866static void HTMLfooter(qgrow_t *reply)
2867{
2868 reply->addstrf(reply, " </div>\n");
2869 reply->addstr(reply,
2870 " <div class='top-right'>\n"
2871 " <h1>Experimental account manager</h1>\n"
2872 " <p>This account manager system is currently experimental, and under heavy development. &nbsp; "
2873 " Which means it's not all written yet, and things may break.</p>\n"
2874 " <p>To create an account, choose a name and password, type them in, click the 'create account' button.</p>"
2875 " <p>On the next page, fill in all your details, then click on the 'confirm' button.</p>"
2876 " <p>We follow the usual web site registration process, which sends a validation email, with a link to click. &nbsp; "
2877 " When you get that email, click on the link, or copy it into a web browser.</p>"
2878 " <p>You will then be logged off. &nbsp; Now you have to wait for an admin to approve your new account. &nbsp; "
2879 " They should check with the person you listed as vouching for you first. &nbsp; They will tell you after they approve your account.</p>"
2880 " <p>Missing bits that are still being written - editing accounts, listing accounts, deleting accounts.</p>\n"
2881 " </div>\n");
2882// reply->addstr(reply, " <div class='centre'>\n </div>\n");
2883 reply->addstr(reply,
2884// " <div class='bottom-left'>\n"
2885// " </div>\n"
2886 " <div class='bottom-right'>\n"
2887 " <iframe src='stats.html' style='border:none;height:100%;width:100%;'></iframe>\n"
2888 " </div>\n"
2889 " </font>\n"
2890 "</body>\n</html>\n");
2891}
2892
2893
2894fragment *newFragment(enum fragmentType type, char *text, int len)
2895{
2896 fragment *frg = xmalloc(sizeof(fragment));
2897
2898 frg->type = type;
2899 frg->length = len;
2900 frg->text = xmalloc(len + 1);
2901 memcpy(frg->text, text, len);
2902 frg->text[len] = '\0';
2903 return frg;
2904}
2905
2906qlist_t *fragize(char *mm, size_t length)
2907{
2908 qlist_t *fragments = qlist(QLIST_THREADSAFE);
2909 fragment *frg0, *frg1;
2910
2911 char *h;
2912 int i, j = 0, k = 0, l, m;
2913
2914 // Scan for server side includes style markings.
2915 for (i = 0; i < length; i++)
2916 {
2917 if (i + 5 < length)
2918 {
2919 if (('<' == mm[i]) && ('!' == mm[i + 1]) && ('-' == mm[i + 2]) && ('-' == mm[i + 3]) && ('#' == mm[i + 4])) // '<!--#'
2920 {
2921 m = i;
2922 i += 5;
2923 if (i < length)
2924 {
2925 if (('e' == mm[i]) && ('c' == mm[i + 1]) && ('h' == mm[i + 2]) && ('o' == mm[i + 3]) && (' ' == mm[i + 4])) // 'echo '
2926 {
2927 i += 5;
2928 if (i + 5 < length)
2929 {
2930 if (('v' == mm[i]) && ('a' == mm[i + 1]) && ('r' == mm[i + 2]) && ('=' == mm[i + 3]) && ('"' == mm[i + 4])) // 'var="'
2931 {
2932 i += 5;
2933 for (j = i; j < length; j++)
2934 {
2935 if ('"' == mm[j]) // '"'
2936 {
2937 frg1 = newFragment(FT_PARAM, &mm[i], j - i);
2938 i = j + 1;
2939 if (i + 4 < length)
2940 {
2941 if ((' ' == mm[i]) && ('-' == mm[i + 1]) && ('-' == mm[i + 2]) && ('>' == mm[i + 3])) // ' -->'
2942 i += 4;
2943 }
2944 frg0 = newFragment(FT_TEXT, &mm[k], m - k);
2945 fragments->addlast(fragments, frg0, sizeof(fragment));
2946 fragments->addlast(fragments, frg1, sizeof(fragment));
2947 free(frg0);
2948 free(frg1);
2949 k = i;
2950 break;
2951 }
2952 }
2953 }
2954 }
2955 }
2956 }
2957 }
2958 }
2959 }
2960 frg0 = newFragment(FT_TEXT, &mm[k], length - k);
2961 fragments->addlast(fragments, frg0, sizeof(*frg0));
2962 free(frg0);
2963
2964 return fragments;
2965}
2966
2967void unfragize(qlist_t *fragments, reqData *Rd, boolean fre)
2968{
2969 qlist_obj_t lobj;
2970 memset((void *) &lobj, 0, sizeof(lobj));
2971 fragments->lock(fragments);
2972 while (fragments->getnext(fragments, &lobj, false) == true)
2973 {
2974 fragment *frg = (fragment *) lobj.data;
2975 if (NULL == frg->text)
2976 {
2977 E("NULL fragment!");
2978 continue;
2979 }
2980 if (NULL != Rd)
2981 HTMLfill(Rd, frg->type, frg->text, frg->length);
2982 if (fre)
2983 free(frg->text);
2984 }
2985 fragments->unlock(fragments);
2986 if (fre)
2987 fragments->free(fragments);
2988}
2989
2990HTMLfile *checkHTMLcache(char *file)
2991{
2992 if (NULL == HTMLfileCache)
2993 HTMLfileCache = qhashtbl(0, 0);
2994
2995 HTMLfile *ret = (HTMLfile *) HTMLfileCache->get(HTMLfileCache, file, NULL, true);
2996 int fd = open(file, O_RDONLY);
2997 size_t length = 0;
2998
2999 if (-1 == fd)
3000 {
3001 HTMLfileCache->remove(HTMLfileCache, file);
3002 free(ret);
3003 ret = NULL;
3004 }
3005 else
3006 {
3007 struct stat sb;
3008 if (fstat(fd, &sb) == -1)
3009 {
3010 HTMLfileCache->remove(HTMLfileCache, file);
3011 free(ret);
3012 ret = NULL;
3013 E("Failed to stat %s", file);
3014 }
3015 else
3016 {
3017 if ((NULL != ret) && (ret->last.tv_sec < sb.st_mtim.tv_sec))
3018 {
3019 HTMLfileCache->remove(HTMLfileCache, file);
3020 free(ret);
3021 ret = NULL;
3022 }
3023
3024 if (NULL == ret)
3025 {
3026 char *mm = MAP_FAILED;
3027
3028 ret = xmalloc(sizeof(HTMLfile));
3029 length = sb.st_size;
3030 ret->last.tv_sec = sb.st_mtim.tv_sec;
3031 ret->last.tv_nsec = sb.st_mtim.tv_nsec;
3032
3033 D("Loading web template %s, which is %d bytes long.", file, length);
3034 mm = mmap(NULL, length, PROT_READ, MAP_SHARED | MAP_POPULATE, fd, 0);
3035 if (mm == MAP_FAILED)
3036 {
3037 HTMLfileCache->remove(HTMLfileCache, file);
3038 free(ret);
3039 ret = NULL;
3040 E("Failed to mmap %s", file);
3041 }
3042 else
3043 {
3044 ret->fragments = fragize(mm, length);
3045 if (-1 == munmap(mm, length))
3046 FCGI_fprintf(FCGI_stderr, "Failed to munmap %s\n", file);
3047
3048 HTMLfileCache->put(HTMLfileCache, file, ret, sizeof(*ret));
3049 }
3050 }
3051 close(fd);
3052 }
3053 }
3054
3055 return ret;
3056}
3057
3058
3059
3060
3061
3062/* TODO -
3063
3064 On new user / password reset.
3065. Both should have all the same security concerns as the login page, they are basically logins.
3066. Codes should be "very long", "(for example, 16 case-sensitive alphanumeric characters)"
3067. "confirm" button hit on "accountCreationPage" or "resetPasswordPage"
3068. generate a new token, keep it around for idleTimeOut (or at least 24 hours), call it .linky instead of .lua
3069. hash the linky for the file name, for the same reason we hash the hashish with pepper for the leaf-node.
3070. Include user level field, new users get -200.
3071. Store the linky itself around somewhere we can find it quickly for logged in users.
3072. store it in the regenerated session
3073. Scratch that, we should never store the raw linky, see above about hashing the linky.
3074
3075. The linky is just like the session token, create it in exactly the same way.
3076. Linky is base64() of the binary, so it's short enough to be a file name, and not too long for the URL.
3077. But we only get to send one of them as a linky URL, no backup cookies / body / headers.
3078. Sooo, need to separate the session stuff out of Rd->stuff.
3079. Use two separate qhashtbl's, Rd->session and Rd->linky.
3080
3081 For new user
3082. create their /opt/opensim_SC/var/lib/users/UUID.lua account record, and symlink firstName_lastName.lua to it.
3083. They can log on,
3084 but all they can do is edit their email to send a new validation code, and enter the validation code.
3085 They can reset their password.
3086. Warn them on login and any page refresh that there is an outstanding validation awaiting them.
3087 For reset password
3088. Let them do things as normal, in case this was just someone being mean to them, coz their email addy might be public.
3089. Including the usual logging out and in again with their old password.
3090. Warn them on login and any page refresh that there is an outstanding password reset awaiting them.
3091. email linky, which is some or all of the token result bits strung together, BASE64 encode the result.
3092. regenerate the usual token
3093. user clicks on the linky (or just enters the linky in a field)
3094. validate the linky token.
3095 compare the level field to the linky type in the linky URL, new users -200 would be "../validateUser/.." and not "../resetPassword/.."
3096. delete the linky token
3097. Particularly important for the forgotten password email, since now that token is in the wild, and is used to reset a password.
3098 Which begs the question, other than being able to recieve the email, how do we tell it's them?
3099 Security questions suck, too easily guessed.
3100 Ask their DoB. Still sucky, coz "hey it's my birthday today" is way too leaky.
3101 This is what Multi Factor Autentication is good for, and that's on the TODO list.
3102 Also, admins should have a harder time doing password resets.
3103 Must be approved by another admin?
3104 Must log onto the server via other means to twiddle something there?
3105 For password reset page
3106 Ask their DoB to help confirm it's them.
3107 validate the DoB, delete tokens and back to the login page if they get it wrong
3108 Should warn people on the accountCreationPage that DoB might be used this way.
3109 ask them for the new password, twice
3110 Create a new passwordSalt and passwordHash, store them in the auth table.
3111. For validate new user page
3112. tell them they have validated
3113. create their OpenSim account UserAccounts.UserTitle and auth tables, not GridUser table
3114. create their GridUser record.
3115. update their UserAccounts.Userlevel and UserAccounts.UserTitle
3116. send them to the login page.
3117. regenerate the usual token
3118? let user stay logged on?
3119 Check best practices for this.
3120
3121 Check password strength.
3122 https://stackoverflow.com/questions/549/the-definitive-guide-to-form-based-website-authentication?rq=1
3123 Has some pointers to resources in the top answers "PART V: Checking Password Strength" section.
3124
3125 "PART VI: Much More - Or: Preventing Rapid-Fire Login Attempts" and "PART VII: Distributed Brute Force Attacks" is also good for -
3126 Login attempt throttling.
3127 Deal with dictionary attacks by slowing down access on password failures etc.
3128
3129 Deal with editing yourself.
3130 Deal with editing others, but only as god.
3131
3132
3133 Regularly delete old session files and ancient newbies.
3134
3135
3136 Salts should be "lengthy" (128 bytes suggested in 2007) and random. Should be per user to. Or use a per user and a global one, and store the global one very securely.
3137 And store hash(salt+password).
3138 On the other hand, the OpenSim / SL password hashing function may be set in concrete in all the viewers. I'll have to find out.
3139 So far I have discovered -
3140 On login server side if the password doesn't start with $1$, then password = "$1$" + Util.Md5Hash(passwd);
3141 remove the "$1$ bit
3142 string hashed = Util.Md5Hash(password + ":" + data.Data["passwordSalt"].ToString());
3143 if (data.Data["passwordHash"].ToString() == hashed)
3144 passwordHash is char(32), and as implied above, doesn't include the $1$ bit. passwordSalt is also char(32)
3145 Cool VL and Impy at least sends the $1$ md5 hashed version over the wire.
3146 Modern viewers obfuscate the details buried deep in C++ OOP crap.
3147 Sent via XMLRPC
3148 MD5 is considered broken since 2013, possibly longer.
3149 Otherwise use a slow hashing function. bcrypt? scrypt? Argon2? PBKDF2?
3150 https://security.stackexchange.com/questions/211/how-to-securely-hash-passwords
3151 Should include a code in the result that tells us which algorithm was used, so we can change the algorithm at a later date. /etc/passwd does this.
3152 Which is what the $1$ bit currently used between server and client is sorta for.
3153
3154+ Would be good to have one more level of this Rd->database stuff, so we know the type of the thing.
3155 While qhashtbl has API for putting strings, ints, and memory, it has none for finding out what type a stored thing is.
3156 Once I have a structure, I add things like "level needed to edit it", "OpenSim db structure to Lua file mapping", and other fanciness.
3157 Would also help with the timestamp being stored for the session, it prints out binary gunk in the DEBUG <div>.
3158 Starting to get into object oriented territory here. B-)
3159 I'll have to do it eventually anyway.
3160 object->tostring(object), and replace the big switch() statements in the existing db code with small functions.
3161 That's why the qLibc stuff has that format, coz C doesn't understand the concept of passing "this" as the first argument.
3162 https://stackoverflow.com/questions/351733/how-would-one-write-object-oriented-code-in-c
3163 https://stackoverflow.com/questions/415452/object-orientation-in-c
3164 http://ooc-coding.sourceforge.net/
3165
3166
3167https://owasp.org/www-project-cheat-sheets/cheatsheets/Input_Validation_Cheat_Sheet.html#Email_Address_Validation
3168https://cheatsheetseries.owasp.org/
3169https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html
3170https://owasp.org/www-project-cheat-sheets/cheatsheets/Authentication_Cheat_Sheet.html
3171https://softwareengineering.stackexchange.com/questions/46716/what-technical-details-should-a-programmer-of-a-web-application-consider-before
3172https://wiki.owasp.org/index.php/OWASP_Guide_Project
3173https://stackoverflow.com/questions/549/the-definitive-guide-to-form-based-website-authentication?rq=1
3174https://owasp.org/www-community/xss-filter-evasion-cheatsheet
3175 A list of example XSS things to try.
3176*/
3177
3178
3179
3180/* Four choices for the token - (https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html)
3181 https://en.wikipedia.org/wiki/Cross-site_request_forgery
3182 Has some more info.
3183
3184Large random value generated by a secure method (getrandom(2)).
3185 Keep it secret, put it in hidden fields, or custom HTTP header (requires JavaScript but more secure than hidden fields).
3186 NOT cookies or GET. Don't log it.
3187Cryptographically sign a session ID and timestamp.
3188 Timestamp is for session timeouts.
3189 Keep it secret, put it in hidden fields, or custom HTTP header (requires JavaScript but more secure than hidden fields).
3190 Needs a secret key server side.
3191A strong HMAC (SHA-256 or better) of a session ID and timestamp.
3192 The above document seems to imply that a key is used for this, the openssl EVP functions don't mention any way of supplying this key.
3193 https://en.wikipedia.org/wiki/HMAC says there is a key as well.
3194 https://www.openssl.org/docs/man1.1.0/man3/HMAC.html HAH! They can have keys. OpenSSL's docs suck.
3195 Token = HMAC(sessionID+timestamp)+timestamp (Yes, timestamp is used twice).
3196 Keep it secret, put it in hidden fields, or custom HTTP header (requires JavaScript but more secure than hidden fields).
3197 Needs a secret key server side.
3198Double cookie
3199 Large random value generated by a secure method set as a cookie and hidden field. Check they match.
3200 Optional - encrypt / salted hash it in another cookie / hidden field.
3201+ Also a resin (BASE64 session key in the query string).
3202 Not such a good idea to have something in the query, coz that screws with bookmarks.
3203 https://security.stackexchange.com/questions/59470/double-submit-cookies-vulnerabilities
3204 Though so far all the pages I find saying this don't say flat out say "use headers instead", though they do say "use HSTS".
3205 https://security.stackexchange.com/questions/220797/is-the-double-submit-cookie-pattern-still-effective
3206+ Includes a work around that I might already be doing.
3207TODO - think it through, is it really secure against session hijacking?
3208TODO - document why we redirect POST to GET, coz it's a pain in the arse, and we have to do things twice.
3209
3210SOOOOO - use double cookie + hidden field.
3211 No headers, coz I need JavaScript to do that.
3212 No hidden field when redirecting post POST to GET, coz GET doesn't get those.
3213 pepper = long pass phrase or some such stored in .sledjChisl.conf.lua, which has to be protected dvs1/opensimsc/0640 as well as the database credentials.
3214 salt = large random value generated by a secure method (getrandom(2)).
3215 seshID = large random value generated by a secure method (getrandom(2)).
3216 timeStamp = mtime of the leaf-node file, set to current time when we are creating the token.
3217 sesh = seshID + timeStamp
3218 munchie = HMAC(sesh) + timeStamp The token hidden field
3219 toke_n_munchie = HMAC(UUID + munchie) The token cookie
3220 hashish = HMACkey(toke_n_munchie, salt) Salted token cookie & linky query
3221? resin = BASE64(hashish) Base64 token cookie
3222 leaf-node = HMACkey(hashish, pepper) Stored token file name
3223
3224 Leaf-node.lua (mtime is timeStamp)
3225 IP, UUID, salt, seshID, user name, passwordSalt, passwordHash (last two for OpenSim password protocol)
3226
3227 The test - (validateSesh() below)
3228 we get hashish and toke_n_munchie
3229 HMACkey(hashish + pepper) -> leaf-node
3230 read leaf-node.lua -> IP, UUID, salt, seshID
3231 get it's mtime -> timeStamp
3232 seshID + timeStamp -> sesh
3233 HMAC(sesh) + timeStamp -> munchie
3234 if we got munchie in the hidden field, compare it
3235 toke_n_munchie == HMAC(UUID + munchie)
3236 For linky it'll be -
3237 HMAC(UUID + munchie) -> toke_n_munchie
3238 hashish == HMACkey(toke_n_munchie + salt)
3239+ If it's too old according to mtime, delete it and logout.
3240
3241I should make it easy to change the HMAC() function. Less important for these short lived sessions, more important for the linky URLs, most important for stored password hashes.
3242 Same for the pepper.
3243
3244The required JavaScript might be like https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#xmlhttprequest--native-javascript-
3245 NOTE - they somehow fucked up that anchor tag.
3246
3247NOTE - storing a pepper on the same RAID array as everything else will be a problem when it comes time to replace one of the disks.
3248 It might have failed badly enough that you can't wipe it, but an attacker can dumpster dive it, replace the broken bit (firmware board), and might get lucky.
3249 Also is a problem with SSD and rust storing good data on bad sectors in the spare sector pool, wear levelling, etc.
3250
3251https://stackoverflow.com/questions/16891729/best-practices-salting-peppering-passwords
3252*/
3253
3254
3255qlisttbl_t *accountLevels = NULL;
3256
3257
3258static void bitch(reqData *Rd, char *message, char *log)
3259{
3260 addStrL(Rd->errors, message);
3261 E("%s %s %s - %s %s", getStrH(Rd->headers, "REMOTE_ADDR"), Rd->shs.UUID, Rd->shs.name, message, log);
3262}
3263
3264/* "A token cookie that references a non-existent session, its value should be replaced immediately to prevent session fixation."
3265https://owasp.org/www-community/attacks/Session_fixation
3266 Which describes the problem, but offers no solution.
3267 See https://stackoverflow.com/questions/549/the-definitive-guide-to-form-based-website-authentication?rq=1.
3268I think this means send a new cookie.
3269 I clear out the cookies and send blank ones with -1 maxAge, so they should get deleted.
3270*/
3271static void bitchSession(reqData *Rd, char *message, char *log)
3272{
3273 if ('\0' != message[0])
3274 addStrL(Rd->errors, message);
3275 C("%s %s %s - %s %s", getStrH(Rd->headers, "REMOTE_ADDR"), Rd->shs.UUID, Rd->shs.name, message, log);
3276 Rd->shs.status = SHS_BOGUS;
3277}
3278
3279
3280// The ancient, insecure since 2011, Second Life / OpenSim password hashing algorithm.
3281char *newSLOSsalt(reqData *Rd)
3282{
3283 char *salt = NULL;
3284 unsigned char *md5hash = xzalloc(17);
3285 char uuid[37];
3286 uuid_t binuuid;
3287
3288 uuid_generate_random(binuuid);
3289 uuid_unparse_lower(binuuid, uuid);
3290 if (!qhashmd5((void *) uuid, strlen(uuid), md5hash))
3291 bitch(Rd, "Internal error.", "newSLOSsalt() - qhashmd5(new uuid) failed.");
3292 else
3293 salt = qhex_encode(md5hash, 16);
3294 free(md5hash);
3295 return salt;
3296}
3297
3298char *checkSLOSpassword(reqData *Rd, char *salt, char *password, char *passwordHash, char *fail)
3299{
3300 char *ret = NULL;
3301 int rt = 0;
3302 unsigned char *md5hash = xzalloc(17);
3303 char *hash = NULL, *passHash = NULL;
3304
3305T("checkSLOSpassword(%s, %s, %s, ", password, salt, passwordHash, fail);
3306 // Calculate passHash.
3307 if (!qhashmd5((void *) password, strlen(password), md5hash))
3308 {
3309 bitch(Rd, "Internal error.", "checkSLOSpassword() - qhashmd5(password) failed.");
3310 rt++;
3311 }
3312 else
3313 {
3314 passHash = qhex_encode(md5hash, 16);
3315 hash = xmprintf("%s:%s", passHash, salt);
3316 if (!qhashmd5((void *) hash, strlen(hash), md5hash))
3317 {
3318 bitch(Rd, "Internal error.", "checkSLOSpassword() - qhashmd5(password:salt) failed.");
3319 rt++;
3320 }
3321 else
3322 {
3323 ret = qhex_encode(md5hash, 16);
3324 }
3325 free(hash);
3326 free(passHash);
3327 }
3328
3329 // If one was passed in, compare it.
3330 if ((NULL != ret) && (NULL != passwordHash) && (strcmp(ret, passwordHash) != 0))
3331 {
3332 bitch(Rd, fail, "Password doesn't match passwordHash");
3333 E(" %s %s - %s != %s", password, salt, ret, passwordHash);
3334 rt++;
3335 free(ret);
3336 ret = NULL;
3337 }
3338 free(md5hash);
3339
3340 return ret;
3341}
3342
3343
3344int LuaToHash(reqData *Rd, char *file, char *var, qhashtbl_t *tnm, int ret, struct stat *st, struct timespec *now, char *type)
3345{
3346 struct timespec then;
3347
3348 if (-1 == clock_gettime(CLOCK_REALTIME, &then))
3349 perror_msg("Unable to get the time.");
3350 I("Reading %s file %s", type, file);
3351 if (0 != stat(file, st))
3352 {
3353 D("No %s file.", file);
3354 perror_msg("Unable to stat %s", file);
3355 ret++;
3356 }
3357 else
3358 {
3359 int status = luaL_loadfile(Rd->L, file), result;
3360
3361 if (status)
3362 {
3363 bitch(Rd, "No such thing.", "Can't load file.");
3364 E("Couldn't load file: %s", lua_tostring(Rd->L, -1));
3365 ret++;
3366 }
3367 else
3368 {
3369 result = lua_pcall(Rd->L, 0, LUA_MULTRET, 0);
3370
3371 if (result)
3372 {
3373 bitch(Rd, "Broken thing.", "Can't run file.");
3374 E("Failed to run script: %s", lua_tostring(Rd->L, -1));
3375 ret++;
3376 }
3377 else
3378 {
3379 lua_getglobal(Rd->L, var);
3380 lua_pushnil(Rd->L);
3381
3382 while(lua_next(Rd->L, -2) != 0)
3383 {
3384 char *n = (char *) lua_tostring(Rd->L, -2);
3385
3386 if (lua_isstring(Rd->L, -1))
3387 {
3388 tnm->putstr(tnm, n, (char *) lua_tostring(Rd->L, -1));
3389d("Lua reading (%s) %s = %s", type, n, getStrH(tnm, n));
3390 }
3391 else
3392 {
3393 char *v = (char *) lua_tostring(Rd->L, -1);
3394 W("Unknown Lua variable type for %s = %s", n, v);
3395 }
3396 lua_pop(Rd->L, 1);
3397 }
3398
3399 if (-1 == clock_gettime(CLOCK_REALTIME, now))
3400 perror_msg("Unable to get the time.");
3401 double n = (now->tv_sec * 1000000000.0) + now->tv_nsec;
3402 double t = (then.tv_sec * 1000000000.0) + then.tv_nsec;
3403 T("Reading %s file took %lf seconds", type, (n - t) / 1000000000.0);
3404 }
3405 }
3406 }
3407
3408 return ret;
3409}
3410
3411
3412char *checkLinky(reqData *Rd)
3413{
3414// TODO - should be from Rd.shs->linky-hashish
3415 char *ret = xstrdup(""), *t0 = getStrH(Rd->stuff, "linky-hashish");
3416
3417 if ('\0' != t0[0])
3418 {
3419 char *t1 = qurl_encode(t0, strlen(t0));
3420 free(ret);
3421 ret = xmprintf("<p><font color='red'><b>You have an email waiting with a validation link in it, please check your email. &nbsp; "
3422 "It will be from %s@%s, and it might be in your spam folder, coz these sorts of emails sometimes end up there. &nbsp; "
3423 "You should add that email address to your contacts, or otherwise let it through your spam filter. &nbsp; "
3424// "<a href='https://%s%s?hashish=%s'>%s</a>"
3425 "</b></font></p>\n",
3426 "grid_no_reply", Rd->Host,
3427 Rd->Host, Rd->RUri
3428// ,t1, t0
3429 );
3430 free(t1);
3431 }
3432 return ret;
3433}
3434
3435
3436static void freeSesh(reqData *Rd, boolean linky, boolean wipe)
3437{
3438 char *file = NULL;
3439 sesh *shs = &Rd->shs;
3440
3441T("free sesh %s %s", linky ? "linky" : "session", wipe ? "wipe" : "delete");
3442 if (linky)
3443 {
3444 shs = Rd->lnk;
3445 file = xmprintf("%s/sessions/%s.linky", scCache, shs->leaf);
3446 }
3447 else
3448 file = xmprintf("%s/sessions/%s.lua", scCache, shs->leaf);
3449
3450 if (wipe)
3451 I("Wiping session %s.", file);
3452 else
3453 I("Deleting session %s.", file);
3454
3455 if ('\0' != shs->leaf[0])
3456 {
3457 if (unlink(file))
3458 perror_msg("Unable to delete %s", file);
3459 }
3460
3461 Rd->body-> remove(Rd->body, "munchie");
3462
3463 freeCookie(Rd, "toke_n_munchie");
3464 freeCookie(Rd, "hashish");
3465 cookie *ck = setCookie(Rd, "toke_n_munchie", "");
3466 cookie *ckh = setCookie(Rd, "hashish", "");
3467 ck->maxAge = -1; // Should expire immediately.
3468 ckh->maxAge = -1; // Should expire immediately.
3469
3470 qhashtbl_obj_t obj;
3471
3472 if (wipe)
3473 {
3474 memset((void*)&obj, 0, sizeof(obj));
3475 Rd->database->lock(Rd->database);
3476 while(Rd->database->getnext(Rd->database, &obj, false) == true)
3477 Rd->database->remove(Rd->database, obj.name);
3478 Rd->database->unlock(Rd->database);
3479 if (NULL != shs->name) free(shs->name);
3480 shs->name = NULL;
3481 if (NULL != shs->UUID) free(shs->UUID);
3482 shs->UUID = NULL;
3483 shs->level = -256;
3484// TODO - should I wipe the rest of Rd->shs as well?
3485 Rd->stuff->remove(Rd->stuff, "name");
3486 Rd->stuff->remove(Rd->stuff, "firstName");
3487 Rd->stuff->remove(Rd->stuff, "lastName");
3488 Rd->stuff->remove(Rd->stuff, "email");
3489 Rd->stuff->remove(Rd->stuff, "passwordSalt");
3490 Rd->stuff->remove(Rd->stuff, "passwordHash");
3491 Rd->stuff->remove(Rd->stuff, "passHash");
3492 Rd->stuff->remove(Rd->stuff, "passSalt");
3493 Rd->stuff->remove(Rd->stuff, "linky-hashish");
3494 }
3495
3496 if (shs->isLinky)
3497 {
3498 free(Rd->lnk);
3499 Rd->lnk = NULL;
3500 }
3501 else
3502 {
3503 shs->leaf[0] = '\0';
3504 }
3505 free(file);
3506}
3507
3508static void setToken_n_munchie(reqData *Rd, boolean linky)
3509{
3510 sesh *shs = &Rd->shs;
3511 char *file;
3512
3513 if (linky)
3514 {
3515 shs = Rd->lnk;
3516 file = xmprintf("%s/sessions/%s.linky", scCache, shs->leaf);
3517 }
3518 else
3519 {
3520 file = xmprintf("%s/sessions/%s.lua", scCache, shs->leaf);
3521 }
3522
3523 struct stat st;
3524 int s = stat(file, &st);
3525
3526 if (!linky)
3527 {
3528 setCookie(Rd, "toke_n_munchie", shs->toke_n_munchie);
3529 setCookie(Rd, "hashish", shs->hashish);
3530 }
3531 char *tnm0 = xmprintf( "toke_n_munchie = \n"
3532 "{\n"
3533 " ['IP']='%s',\n"
3534 " ['salt']='%s',\n"
3535 " ['seshID']='%s',\n",
3536 getStrH(Rd->headers, "REMOTE_ADDR"),
3537 shs->salt,
3538 shs->seshID
3539 );
3540 char *tnm1 = xmprintf(" ['name']='%s',\n ['level']='%d',\n", shs->name, (int) shs->level);
3541 char *tnm2 = xmprintf(" ['UUID']='%s',\n", shs->UUID);
3542 char *tnm3 = xmprintf(" ['passHash']='%s',\n", getStrH(Rd->stuff, "passHash"));
3543 char *tnm4 = xmprintf(" ['passSalt']='%s',\n", getStrH(Rd->stuff, "passSalt"));
3544 char *tnm9 = xmprintf("}\n"
3545 "return toke_n_munchie\n");
3546 int fd = notstdio(xcreate_stdio(file, O_CREAT | O_WRONLY | O_TRUNC | O_CLOEXEC, S_IRUSR | S_IWUSR));
3547 size_t l = strlen(tnm0);
3548
3549 if (s)
3550 I("Creating session %s.", file);
3551 else
3552 C("Updating session %s.", file); // I don't think updates can occur now.
3553t("Write shs %s", tnm0);
3554 if (l != writeall(fd, tnm0, l))
3555 {
3556 perror_msg("Writing %s", file);
3557 freeSesh(Rd, linky, TRUE);
3558 }
3559
3560 if (NULL != shs->name)
3561 {
3562t("Write shs %s", tnm1);
3563 l = strlen(tnm1);
3564 if (l != writeall(fd, tnm1, l))
3565 {
3566 perror_msg("Writing %s", file);
3567 freeSesh(Rd, linky, TRUE);
3568 }
3569 }
3570 if (NULL != shs->UUID)
3571 {
3572t("Write shs %s", tnm2);
3573 l = strlen(tnm2);
3574 if (l != writeall(fd, tnm2, l))
3575 {
3576 perror_msg("Writing %s", file);
3577 freeSesh(Rd, linky, TRUE);
3578 }
3579 }
3580
3581 if ('\0' != getStrH(Rd->stuff, "passHash")[0])
3582 {
3583t("Write shs %s", tnm3);
3584 l = strlen(tnm3);
3585 if (l != writeall(fd, tnm3, l))
3586 {
3587 perror_msg("Writing %s", file);
3588 freeSesh(Rd, linky, TRUE);
3589 }
3590 }
3591
3592 if ('\0' != getStrH(Rd->stuff, "passSalt")[0])
3593 {
3594t("Write shs %s", tnm4);
3595 l = strlen(tnm4);
3596 if (l != writeall(fd, tnm4, l))
3597 {
3598 perror_msg("Writing %s", file);
3599 freeSesh(Rd, linky, TRUE);
3600 }
3601 }
3602
3603 l = strlen(tnm9);
3604 if (l != writeall(fd, tnm9, l))
3605 {
3606 perror_msg("Writing %s", file);
3607 freeSesh(Rd, linky, TRUE);
3608 }
3609 // Set the mtime on the file.
3610 futimens(fd, shs->timeStamp);
3611 xclose(fd);
3612 free(tnm9);
3613 free(tnm4);
3614 free(tnm3);
3615 free(tnm2);
3616 free(tnm1);
3617 free(tnm0);
3618 free(file);
3619
3620 if (linky)
3621 {
3622// TODO - Later use libcurl.
3623 char *first = getStrH(Rd->stuff, "firstName"), *last = getStrH(Rd->stuff, "lastName");
3624// TODO - should be from Rd.shs->linky-hashish
3625 char *t0 = xstrdup(Rd->lnk->hashish), *content, *command;
3626
3627 if ('\0' != t0[0])
3628 {
3629 size_t sz = qhex_decode(t0);
3630 char *t1 = qB64_encode(t0, sz);
3631
3632 content = xmprintf(
3633 "From: grid_no_reply@%s\n"
3634 "Relpy-to: grid_no_reply@%s\n"
3635 "Return-Path: bounce_email@%s\n"
3636 "To: %s\n"
3637 "Subject: Validate your new account on %s\n"
3638 "\n"
3639 "This is an automated validation email sent from %s.\n"
3640 "\n"
3641 "Dear %s %s,\n"
3642 "\n"
3643 "Some one has created the account '%s %s' on \n"
3644 "https://%s%s, and hopefully it was you.\n"
3645 "If it wasn't you, you can ignore this email.\n"
3646 "\n"
3647 "Please go to this web link to validate your new account -\n"
3648 "https://%s%s?hashish=%s\n"
3649 "\n"
3650 "Do not reply to this email.\n"
3651 "\n",
3652 Rd->Host, Rd->Host, Rd->Host,
3653 getStrH(Rd->stuff, "email"),
3654 Rd->Host, Rd->Host,
3655 first, last,
3656 first, last, Rd->Host, Rd->RUri,
3657 Rd->Host, Rd->RUri, t1
3658 );
3659 l = strlen(content);
3660 file = xmprintf("%s/sessions/%s.email", scCache, shs->leaf);
3661 fd = notstdio(xcreate_stdio(file, O_CREAT | O_WRONLY | O_TRUNC | O_CLOEXEC, S_IRUSR | S_IWUSR));
3662
3663 if (l != writeall(fd, content, l))
3664 {
3665 perror_msg("Writing %s", file);
3666// freeSesh(Rd, linky, TRUE);
3667 }
3668 xclose(fd);
3669 I("Sending linky email to %s %s", getStrH(Rd->stuff, "email"), t1);
3670 command = xmprintf("sendmail -oi -t <'%s'", file);
3671 int i = system(command);
3672 if (!WIFEXITED(i))
3673 E("sendmail command failed!");
3674 free(command);
3675 free(file);
3676 free(content);
3677 free(t1);
3678 free(t0);
3679 }
3680 }
3681}
3682
3683
3684static void generateAccountUUID(reqData *Rd)
3685{
3686 // Generate a UUID, check it isn't already being used.
3687 char uuid[37], *where;
3688 uuid_t binuuid;
3689 my_ulonglong users = 0;
3690 int c;
3691
3692 do // UserAccounts.PrincipalID is a unique primary index anyway, but we want the user creation process to be a little on the slow side.
3693 {
3694 struct stat st;
3695
3696 uuid_generate_random(binuuid);
3697 uuid_unparse_lower(binuuid, uuid);
3698 // Try Lua user file.
3699 where = xmprintf("%s/users/%s.lua", scData, uuid);
3700 c = stat(where, &st);
3701 if (c)
3702 users = 1;
3703 free(where);
3704 // Try database.
3705 where = xmprintf("UserAccounts.PrincipalID = '%s'", uuid);
3706 D("Trying new UUID %s.", where);
3707 users = dbCount("UserAccounts", where);
3708 free(where);
3709 } while (users != 0);
3710 if (NULL != Rd->shs.UUID) free(Rd->shs.UUID);
3711 Rd->shs.UUID = xstrdup(uuid);
3712 Rd->shs.level = -200;
3713 Rd->database->putstr(Rd->database, "UserAccounts.PrincipalID", uuid);
3714 Rd->database->putstr(Rd->database, "UserAccounts.Userlevel", "-200");
3715}
3716
3717char *getLevel(short level)
3718{
3719 char *ret = "", *lvl = xmprintf("%d", level);
3720 ret = accountLevels->getstr(accountLevels, lvl, false);
3721 if (NULL == ret)
3722 {
3723 qlisttbl_obj_t obj;
3724
3725 memset((void*)&obj, 0, sizeof(obj)); // must be cleared before call
3726 accountLevels->lock(accountLevels);
3727 while(accountLevels->getnext(accountLevels, &obj, NULL, false) == true)
3728 {
3729 if (atoi(obj.name) <= level)
3730 ret = (char *) obj.data;
3731 }
3732 }
3733 free(lvl);
3734 return ret;
3735}
3736
3737typedef struct _systemFolders systemFolders;
3738struct _systemFolders
3739{
3740 char *name;
3741 short type;
3742};
3743
3744systemFolders sysFolders[] =
3745{
3746 {"My Inventory", 8},
3747 {"Animations", 20},
3748 {"Body Parts", 13},
3749 {"Calling Cards", 2},
3750// {"Friends", 2},
3751// {"All", 2},
3752 {"Clothing", 5},
3753 {"Current Outfit", 46},
3754 {"Favorites", 23},
3755 {"Gestures", 21},
3756 {"Landmarks", 3},
3757 {"Lost And Found", 16},
3758 {"Mesh", 49},
3759 {"My Outfits", 48},
3760 {"My Suitcase", 100}, // All the others are replicated inside.
3761 {"Notecards", 7},
3762 {"Objects", 6},
3763 {"Outfit", 47},
3764 {"Photo Album", 15},
3765 {"Scripts", 10},
3766 {"Sounds", 1},
3767 {"Textures", 0},
3768 {"Trash", 14},
3769 {NULL, -1}
3770};
3771
3772boolean writeLuaDouble(reqData *Rd, int fd, char *file, char *name, double number)
3773{
3774 boolean ret = TRUE;
3775// TODO - putting these in Lua as numbers causes lua_tolstring to barf when we read them. Though Lua is supposed to convert between numbers and strings.
3776 char *t = xmprintf(" ['%s'] = '%f',\n", name, number); // NOTE - default precision is 6 decimal places.
3777 size_t l = strlen(t);
3778
3779 if (l != writeall(fd, t, l))
3780 {
3781 perror_msg("Writing %s", file);
3782 ret = FALSE;
3783 }
3784 free(t);
3785 return ret;
3786}
3787
3788boolean writeLuaInteger(reqData *Rd, int fd, char *file, char *name, long number)
3789{
3790 boolean ret = TRUE;
3791// TODO - putting these in Lua as numbers causes lua_tolstring to barf when we read them. Though Lua is supposed to convert between numbers and strings.
3792 char *t = xmprintf(" ['%s'] = '%ld',\n", name, number);
3793 size_t l = strlen(t);
3794
3795 if (l != writeall(fd, t, l))
3796 {
3797 perror_msg("Writing %s", file);
3798 ret = FALSE;
3799 }
3800 free(t);
3801 return ret;
3802}
3803
3804boolean writeLuaString(reqData *Rd, int fd, char *file, char *name, char *string)
3805{
3806 boolean ret = TRUE;
3807
3808 if (NULL == string)
3809 string = getStrH(Rd->stuff, name);
3810
3811 size_t l = strlen(string);
3812 char *t0 = xmalloc(l * 2 + 1);
3813 int i, j = 0;
3814
3815// TODO - maybe escape other non printables as well?
3816 for (i = 0; i < l; i++)
3817 {
3818 // We don't need to escape [] here, coz we are using '' below. Same applies to ", but do it anyway.
3819 switch(string[i])
3820 {
3821 case '\n':
3822 case '\\':
3823 case '\'':
3824 case '"':
3825 t0[j++] = '\\'; break;
3826 }
3827 if ('\n' == string[i])
3828 t0[j++] = 'n';
3829 else if ('\r' == string[i])
3830 ;
3831 else
3832 t0[j++] = string[i];
3833 }
3834 t0[j] = '\0';
3835
3836 char *t1 = xmprintf(" ['%s'] = '%s',\n", name, t0);
3837
3838 l = strlen(t1);
3839 if (l != writeall(fd, t1, l))
3840 {
3841 perror_msg("Writing %s to %s", name, file);
3842 ret = FALSE;
3843 }
3844 free(t1);
3845 free(t0);
3846 return ret;
3847}
3848
3849static void accountWrite(reqData *Rd)
3850{
3851 char *uuid = getStrH(Rd->database, "UserAccounts.PrincipalID");
3852 char *file = xmprintf("%s/users/%s.lua", scData, uuid);
3853 char *level = getStrH(Rd->database, "UserAccounts.UserLevel");
3854 char *link = (NULL == Rd->lnk) ? "" : Rd->lnk->hashish;
3855 char *tnm = "user = \n{\n";
3856
3857 struct stat st;
3858 int s = stat(file, &st);
3859 int fd = notstdio(xcreate_stdio(file, O_CREAT | O_WRONLY | O_TRUNC | O_CLOEXEC, S_IRUSR | S_IWUSR));
3860 size_t l = strlen(tnm);
3861 uuid_t binuuid;
3862
3863 uuid_clear(binuuid);
3864 if ((NULL != uuid) && ('\0' != uuid[0]))
3865 uuid_parse(uuid, binuuid);
3866 if ((NULL != uuid) && ('\0' != uuid[0]) && (!uuid_is_null(binuuid)))
3867 {
3868 if (s)
3869 I("Creating user %s.", file);
3870 else
3871 I("Updating user %s.", file);
3872
3873 if (l != writeall(fd, tnm, l))
3874 perror_msg("Writing %s", file);
3875 else
3876 {
3877 char *name = Rd->stuff->getstr(Rd->stuff, "name", true);
3878 char *end = "}\nreturn user\n";
3879
3880 if (!writeLuaString (Rd, fd, file, "name", name)) goto notWritten;
3881 if (!writeLuaInteger(Rd, fd, file, "created",
3882 (strcmp("", getStrH(Rd->stuff, "created")) != 0) ? atol(getStrH(Rd->stuff, "created")) : (long) Rd->shs.timeStamp[1].tv_sec)) goto notWritten;
3883 if (!writeLuaString (Rd, fd, file, "email", NULL)) goto notWritten;
3884 if (!writeLuaString (Rd, fd, file, "title", getLevel(atoi(level)))) goto notWritten;
3885 if (!writeLuaString (Rd, fd, file, "level", level)) goto notWritten;
3886 if (!writeLuaInteger(Rd, fd, file, "flags", 0)) goto notWritten;
3887 if (!writeLuaInteger(Rd, fd, file, "active", 1)) goto notWritten;
3888 if (!writeLuaString (Rd, fd, file, "passwordHash", NULL)) goto notWritten;
3889 if (!writeLuaString (Rd, fd, file, "passwordSalt", NULL)) goto notWritten;
3890 if (!writeLuaString (Rd, fd, file, "UUID", uuid)) goto notWritten;
3891 if (!writeLuaString (Rd, fd, file, "DoB", NULL)) goto notWritten;
3892 if (!writeLuaString (Rd, fd, file, "agree", NULL)) goto notWritten;
3893 if (!writeLuaString (Rd, fd, file, "adult", NULL)) goto notWritten;
3894 if (!writeLuaString (Rd, fd, file, "aboutMe", NULL)) goto notWritten;
3895 if (!writeLuaString (Rd, fd, file, "vouched", "off")) goto notWritten;
3896 if (!writeLuaString (Rd, fd, file, "voucher", NULL)) goto notWritten;
3897 if (!writeLuaString (Rd, fd, file, "link", link)) goto notWritten;
3898 l = strlen(end);
3899 if (l != writeall(fd, end, l))
3900 perror_msg("Writing %s", file);
3901 else
3902 {
3903 char *nm = xmprintf("%s/users/%s.lua", scData, qstrreplace("tr", name, " ", "_"));
3904
3905 free(file);
3906 file = xmprintf("%s.lua", uuid);
3907 I("Symlinking %s to %s", file, nm);
3908 if (0 != symlink(file, nm))
3909 perror_msg("Symlinking %s to %s", file, nm);
3910 free(nm);
3911 }
3912notWritten:
3913 free(name);
3914 }
3915 xclose(fd);
3916
3917 short lvl = atoi(level);
3918
3919 if (0 <= lvl) // Note that http://opensimulator.org/wiki/Userlevel claims that 1 and above are "GOD_LIKE".
3920 {
3921 if (Rd->fromDb)
3922 {
3923 I("Updating database user %s.", getStrH(Rd->stuff, "name"));
3924 }
3925 else
3926 {
3927 // Setup the database stuff.
3928 static dbRequest *acntsI = NULL;
3929 if (NULL == acntsI)
3930 {
3931 static char *szi[] =
3932 {
3933 "FirstName",
3934 "LastName",
3935 "Email",
3936 "Created",
3937 "PrincipalID",
3938 "ScopeID",
3939 "UserLevel",
3940 "UserFlags",
3941 "UserTitle",
3942// "ServiceURLs", // No worky "text", filled with crap.
3943 "active",
3944 NULL
3945 };
3946 static char *szo[] = {NULL};
3947 acntsI = xzalloc(sizeof(dbRequest));
3948 acntsI->table = "UserAccounts";
3949 acntsI->inParams = szi;
3950 acntsI->outParams = szo;
3951 acntsI->where = "";
3952 acntsI->type = CT_CREATE;
3953 dbRequests->addfirst(dbRequests, &acntsI, sizeof(dbRequest *));
3954 }
3955 static dbRequest *authI = NULL;
3956 if (NULL == authI)
3957 {
3958 static char *szi[] = {"UUID", "passwordSalt", "passwordHash", "accountType", "webLoginKey", NULL};
3959 static char *szo[] = {NULL};
3960 authI = xzalloc(sizeof(dbRequest));
3961 authI->table = "auth";
3962 authI->inParams = szi;
3963 authI->outParams = szo;
3964 authI->where = "";
3965 authI->type = CT_CREATE;
3966 dbRequests->addfirst(dbRequests, &authI, sizeof(dbRequest *));
3967 }
3968 static dbRequest *invFolderI = NULL;
3969 if (NULL == invFolderI)
3970 {
3971 static char *szi[] =
3972 {
3973 "agentID",
3974 "folderName",
3975 "type", // smallint(6)
3976 "version", // int(11)
3977 "folderID",
3978 "parentFolderID",
3979 NULL
3980 };
3981 static char *szo[] = {NULL};
3982 invFolderI = xzalloc(sizeof(dbRequest));
3983 invFolderI->table = "inventoryfolders";
3984 invFolderI->inParams = szi;
3985 invFolderI->outParams = szo;
3986 invFolderI->where = "";
3987 invFolderI->type = CT_CREATE;
3988 dbRequests->addfirst(dbRequests, &invFolderI, sizeof(dbRequest *));
3989 }
3990 static dbRequest *gUserI = NULL;
3991 if (NULL == gUserI)
3992 {
3993// static char *szi[] = {"UserID", "HomeRegionID", "HomePosition", "HomeLookAt", "LastRegionID", "LastPosition", "LastLookAt", "Online", "Login", "Logout", NULL};
3994 static char *szi[] = {"UserID", NULL}; // All the defaults are what we would set anyway.
3995 static char *szo[] = {NULL};
3996 gUserI = xzalloc(sizeof(dbRequest));
3997 gUserI->table = "GridUser";
3998 gUserI->inParams = szi;
3999 gUserI->outParams = szo;
4000 gUserI->where = "";
4001 gUserI->type = CT_CREATE;
4002 dbRequests->addfirst(dbRequests, &gUserI, sizeof(dbRequest *));
4003 }
4004
4005 I("Creating database user %s %s.", uuid, getStrH(Rd->stuff, "name"));
4006 char *first = Rd->stuff->getstr(Rd->stuff, "name", true), *last = strchr(first, ' ');
4007
4008 // create user record.
4009 *last++ = '\0';
4010 if (0 != dbDoSomething(acntsI, FALSE,
4011 first,
4012 last,
4013 getStrH(Rd->stuff, "email"),
4014 (strcmp("", getStrH(Rd->stuff, "created")) != 0) ? atoi(getStrH(Rd->stuff, "created")) : (int) Rd->shs.timeStamp[1].tv_sec,
4015 uuid,
4016 "00000000-0000-0000-0000-000000000000",
4017 atoi(level),
4018 0,
4019 getLevel(atoi(level)),
4020// "", // Defaults to NULL, empty string seems OK to. Then gets filled in later.
4021 1
4022 ))
4023 bitch(Rd, "Internal error.", "Failed to create UserAccounts record.");
4024 else
4025 {
4026 char uuidI[37], uuidR[37], uuidC[37], uuidS[37];
4027 uuid_t binuuidI;
4028 int r = 0, i;
4029
4030 // Create inventory records.
4031 strcpy(uuidR, "00000000-0000-0000-0000-000000000000");
4032 for (i = 0; (NULL != sysFolders[i].name) && (0 == r); i++)
4033 {
4034 uuid_generate_random(binuuidI);
4035 uuid_unparse_lower(binuuidI, uuidI);
4036// TODO - should check there isn't a folder with this UUID already.
4037 D("Creating %s inventory folder for user %s.", sysFolders[i].name, getStrH(Rd->stuff, "name"));
4038 r += dbDoSomething(invFolderI, FALSE, uuid, sysFolders[i].name, sysFolders[i].type, 1, uuidI, uuidR); // LEAKY
4039 if (0 != r)
4040 bitch(Rd, "Internal error.", "Failed to create invenoryFolder record.");
4041 if (strcmp("My Inventory", sysFolders[i].name) == 0)
4042 strcpy(uuidR, uuidI);
4043 if (strcmp("Calling Cards", sysFolders[i].name) == 0)
4044 strcpy(uuidC, uuidI);
4045 if (strcmp("My Suitcase", sysFolders[i].name) == 0)
4046 strcpy(uuidS, uuidI);
4047 }
4048
4049 uuid_generate_random(binuuidI);
4050 uuid_unparse_lower(binuuidI, uuidI);
4051// TODO - should check there isn't a folder with this UUID already.
4052 D("Creating %s inventory folder for user %s.", "Friends", getStrH(Rd->stuff, "name"));
4053 r += dbDoSomething(invFolderI, FALSE, uuid, "Friends", 2, 1, uuidI, uuidC);
4054 if (0 != r)
4055 bitch(Rd, "Internal error.", "Failed to create invenoryFolder record.");
4056 strcpy(uuidC, uuidI);
4057
4058 uuid_generate_random(binuuidI);
4059 uuid_unparse_lower(binuuidI, uuidI);
4060// TODO - should check there isn't a folder with this UUID already.
4061 D("Creating %s inventory folder for user %s.", "All", getStrH(Rd->stuff, "name"));
4062 r += dbDoSomething(invFolderI, FALSE, uuid, "All", 2, 1, uuidI, uuidC);
4063 if (0 != r)
4064 bitch(Rd, "Internal error.", "Failed to create invenoryFolder record.");
4065
4066 for (i = 1; (NULL != sysFolders[i].name) && (0 == r); i++)
4067 {
4068 uuid_generate_random(binuuidI);
4069 uuid_unparse_lower(binuuidI, uuidI);
4070// TODO - should check there isn't a folder with this UUID already.
4071 D("Creating %s inventory folder for user %s.", sysFolders[i].name, getStrH(Rd->stuff, "name"));
4072 r += dbDoSomething(invFolderI, FALSE, uuid, sysFolders[i].name, sysFolders[i].type, 1, uuidI, uuidS);
4073 if (0 != r)
4074 bitch(Rd, "Internal error.", "Failed to create invenoryFolder record.");
4075 }
4076
4077 if (0 == r)
4078 {
4079 // Create location record.
4080 D("Creating home and last positions for user %s.", getStrH(Rd->stuff, "name"));
4081 if (0 != dbDoSomething(gUserI, FALSE, uuid)) // LEAKY
4082 bitch(Rd, "Internal error.", "Failed to create GridUser record.");
4083 else
4084 {
4085 // Finally create auth record, so they can log in.
4086 D("Creating auth record for user %s %s.", uuid, getStrH(Rd->stuff, "name"));
4087 if (0 != dbDoSomething(authI, FALSE, uuid, getStrH(Rd->stuff, "passwordSalt"), getStrH(Rd->stuff, "passwordHash"), "UserAccount", "00000000-0000-0000-0000-000000000000"))
4088 bitch(Rd, "Internal error.", "Failed to create auth record.");
4089 }
4090 }
4091
4092 // load iar -m first last / password /opt/opensim_SC/backups/DefaultMember.IAR
4093 simList *sims = getSims();
4094 struct sysinfo info;
4095 float la;
4096
4097 sysinfo(&info);
4098 la = info.loads[0]/65536.0;
4099
4100 for (i = 0; i < sims->num; i++)
4101 {
4102 char *sim = sims->sims[i], *name = getSimName(sims->sims[i]);
4103
4104 if (checkSimIsRunning(sim))
4105 {
4106 I("Loading default member IAR for %s %s in sim %s, this might take a couple of minutes.", first, last, name);
4107 char *c = xmprintf("%s %s/%s send-keys -t '%s:%d' 'load iar -m %s %s / password /opt/opensim_SC/backups/DefaultMember.IAR' Enter",
4108 Tcmd, scRun, Tsocket, Tconsole, i + 1, first, last);
4109T(c);
4110 int r = system(c);
4111 if (!WIFEXITED(r))
4112 E("tmux load iar command failed!");
4113 else
4114 {
4115// memset(toybuf, 0, sizeof(toybuf));
4116// snprintf(toybuf, sizeof(toybuf), "INITIALIZATION COMPLETE FOR %s", name);
4117// waitTmuxText(name, toybuf);
4118// I("%s is done starting up.", name);
4119// la = waitLoadAverage(la, loadAverageInc, simTimeOut);
4120 }
4121 free(c);
4122 free(name);
4123 break;
4124 }
4125 free(name);
4126 }
4127 freeSimList(sims);
4128 }
4129 free(first);
4130 }
4131 }
4132 }
4133 else
4134 W("Not writing NULL UUID user!");
4135 free(file);
4136}
4137
4138static sesh *newSesh(reqData *Rd, boolean linky)
4139{
4140 unsigned char *md5hash = xzalloc(17);
4141 char *toke_n_munchie, *munchie, *hashish, *t0, *t1;
4142 char uuid[37];
4143 uuid_t binuuid;
4144 sesh *ret = &Rd->shs;
4145
4146T("new sesh %s %s %s", linky ? "linky" : "session", ret->UUID, ret->name);
4147 if (linky)
4148 {
4149 Rd->lnk = xzalloc(sizeof(sesh));
4150 ret = Rd->lnk;
4151 ret->UUID = Rd->shs.UUID;
4152 }
4153
4154 char buf[128]; // 512 bits.
4155 int numBytes = getrandom((void *)buf, sizeof(buf), GRND_NONBLOCK);
4156
4157 // NOTE that getrandom() returns random bytes, which may include '\0'.
4158 if (-1 == numBytes)
4159 {
4160 perror_msg("Unable to generate a suitable random number.");
4161 // EAGAIN - not enough entropy, try again.
4162 // EINTR - signal handler interrupted it, try again.
4163 }
4164 else
4165 {
4166 t0 = qhex_encode(buf, sizeof(buf));
4167 qstrcpy(ret->salt, sizeof(ret->salt), t0);
4168 free(t0);
4169//d("salt %s", ret->salt);
4170 numBytes = getrandom((void *)buf, sizeof(buf), GRND_NONBLOCK);
4171 if (-1 == numBytes)
4172 perror_msg("Unable to generate a suitable random number.");
4173 else
4174 {
4175 t0 = qhex_encode(buf, sizeof(buf));
4176 qstrcpy(ret->seshID, sizeof(ret->seshID), t0);
4177 free(t0);
4178//d("seshID %s", ret->seshID);
4179
4180 ret->timeStamp[0].tv_nsec = UTIME_OMIT;
4181 ret->timeStamp[0].tv_sec = UTIME_OMIT;
4182 if (-1 == clock_gettime(CLOCK_REALTIME, &ret->timeStamp[1]))
4183 perror_msg("Unable to get the time.");
4184 else
4185 {
4186 // tv_sec is a time_t, tv_nsec is a long, but the actual type of time_t isn't well defined, it's some sort of integer.
4187 t0 = xmprintf("%s%ld.%ld", ret->seshID, (long) ret->timeStamp[1].tv_sec, ret->timeStamp[1].tv_nsec);
4188 qstrcpy(ret->sesh, sizeof(ret->sesh), t0);
4189//d("sesh %s", ret->sesh);
4190 t1 = myHMAC(t0, FALSE);
4191 free(t0);
4192 munchie = xmprintf("%s%ld.%ld", t1, (long) ret->timeStamp[1].tv_sec, ret->timeStamp[1].tv_nsec);
4193 free(t1);
4194 qstrcpy(ret->munchie, sizeof(ret->munchie), munchie);
4195//d("munchie %s", ret->munchie);
4196// TODO - chicken and egg? Used to be from stuff->UUID.
4197 t1 = ret->UUID;
4198 if (NULL == t1)
4199 {
4200 uuid_clear(binuuid);
4201 uuid_unparse_lower(binuuid, uuid);
4202 ret->UUID = xstrdup(uuid);
4203 }
4204 t0 = xmprintf("%s%s", ret->UUID, munchie);
4205 free(munchie);
4206 toke_n_munchie = myHMAC(t0, FALSE);
4207 free(t0);
4208 qstrcpy(ret->toke_n_munchie, sizeof(ret->toke_n_munchie), toke_n_munchie);
4209//d("toke_n_munchie %s", ret->toke_n_munchie);
4210 hashish = myHMACkey(ret->salt, toke_n_munchie, FALSE);
4211 free(toke_n_munchie);
4212 qstrcpy(ret->hashish, sizeof(ret->hashish), hashish);
4213//d("hashish %s", ret->hashish);
4214 t0 = myHMACkey(getStrH(Rd->configs, "pepper"), hashish, TRUE);
4215 free(hashish);
4216 qstrcpy(ret->leaf, sizeof(ret->leaf), t0);
4217//d("leaf %s", ret->leaf);
4218 free(t0);
4219 ret->isLinky = linky;
4220 setToken_n_munchie(Rd, linky);
4221 }
4222 }
4223 }
4224
4225 free(md5hash);
4226 return ret;
4227}
4228
4229
4230
4231
4232/* CRUD (Create, Read, Update, Delete)
4233CRAP (Create, Replicate, Append, Process)
4234Though I prefer -
4235DAVE (Delete, Add, View, Edit), coz the names are shorter. B-)
4236On the other hand, list or browse needs to be added, which is why they have
4237BREAD (Browse, Read, Edit, Add, Delete)
4238CRUDL (Create, Read, Update, Delete, List)
4239CRUDE (Create, Read, Update, Delete, Experience)
4240Maybe -
4241DAVEE (Delete, Add, View, Edit, Explore)
4242*/
4243
4244// lua.h has LUA_T* NONE, NIL, BOOLEAN, LIGHTUSERDATA, NUMBER, STRING, TABLE, FUNCTION, USERDATA, THREAD as defines, -1 - 8.
4245// These are the missing ones. Then later we will have prim, mesh, script, sound, terrain, ...
4246#define LUA_TGROUP 42
4247#define LUA_TINTEGER 43
4248#define LUA_TEMAIL 44
4249#define LUA_TPASSWORD 45
4250#define LUA_TFILE 46
4251#define LUA_TIMAGE 47
4252
4253#define FLD_NONE 0
4254#define FLD_EDITABLE 1
4255#define FLD_HIDDEN 2
4256#define FLD_REQUIRED 4
4257
4258typedef struct _inputField inputField;
4259typedef struct _inputSub inputSub;
4260typedef struct _inputForm inputForm;
4261typedef struct _inputValue inputValue;
4262
4263typedef int (*inputFieldValidFunc) (reqData *Rd, inputForm *iF, inputValue *iV);
4264typedef void (*inputFieldShowFunc) (reqData *Rd, inputForm *iF, inputValue *iV);
4265typedef int (*inputSubmitFunc) (reqData *Rd, inputForm *iF, inputValue *iV);
4266typedef void (*inputFormShowFunc) (reqData *Rd, inputForm *iF, inputValue *iV);
4267
4268struct _inputField
4269{
4270 char *name, *title, *help;
4271 inputFieldValidFunc validate; // Alas C doesn't have any anonymous function standard.
4272 inputFieldShowFunc web, console, gui;
4273 inputField **group; // If this is a LUA_TGROUP, then this will be a null terminated array of the fields in the group.
4274// database details
4275// lua file details
4276 signed char type, flags;
4277 short editLevel, viewLevel, viewLength, maxLength;
4278};
4279struct _inputSub
4280{
4281 char *name, *title, *help, *outputForm;
4282 inputSubmitFunc submit;
4283};
4284struct _inputForm
4285{
4286 char *name, *title, *help;
4287 qlisttbl_t *fields; // qlisttbl coz iteration in order and lookup are important.
4288 qhashtbl_t *subs;
4289 inputFormShowFunc web, eWeb; // display web, console, gui;
4290// read function
4291// write function
4292};
4293struct _inputValue
4294{
4295 inputField *field;
4296 void *value; // If this is a LUA_TGROUP, then this will be a null.
4297 short valid; // 0 for not yet validated, negative for invalid, positive for valid.
4298 short source, index;
4299};
4300
4301
4302static int sessionValidate(reqData *Rd, inputForm *iF, inputValue *iV)
4303{
4304 int ret = 0;
4305 boolean linky = FALSE;
4306 char *toke_n_munchie = "", *munchie = "", *hashish = "", *leaf = "", *timeStamp = "", *seshion = "", *seshID = "", *t0, *t1;
4307
4308 // In this case the session stuff has to come from specific places.
4309 hashish = Rd->queries->getstr(Rd->queries, "hashish", true);
4310//d("O hashish %s", hashish);
4311 if (NULL != hashish)
4312 {
4313 char *t = xstrdup(hashish);
4314 size_t sz = qB64_decode(t);
4315
4316// TODO - should validate the cookie version as well, if it was sent.
4317// Coz it later tries to delete the linky as if it was the cookie session, and might give us a chance to delete the old session.
4318// Though only if there's a munchie in the body?
4319 I("Validating LINKY hashish %s", hashish);
4320 free(hashish);
4321 hashish = qhex_encode(t, sz);
4322 free(t);
4323 linky = TRUE;
4324 }
4325 else
4326 {
4327 toke_n_munchie = getStrH(Rd->cookies, "toke_n_munchie");
4328// munchie = getStrH(Rd->body, "munchie");
4329 hashish = Rd->cookies->getstr(Rd->cookies, "hashish", true);
4330 if (('\0' == toke_n_munchie[0]) || ((NULL == hashish)))
4331 {
4332 if (strcmp("logout", Rd->doit) == 0)
4333 {
4334 I("Not checking session, coz we are logging out.");
4335 Rd->shs.status = SHS_NUKE;
4336 return ret;
4337 }
4338 bitchSession(Rd, "Invalid session.", "No or blank hashish or toke_n_munchie.");
4339 Rd->shs.status = SHS_NONE;
4340 ret++;
4341 }
4342 else
4343 I("Validating SESSION hashish %s", hashish);
4344 }
4345
4346//d("O toke_n_munchie %s", toke_n_munchie);
4347//d("O munchie %s", munchie);
4348 if (0 == ret)
4349 {
4350 struct stat st;
4351 struct timespec now;
4352
4353 leaf = myHMACkey(getStrH(Rd->configs, "pepper"), hashish, TRUE);
4354//d("leaf %s", leaf);
4355 if (linky)
4356 t0 = xmprintf("%s/sessions/%s.linky", scCache, leaf);
4357 else
4358 t0 = xmprintf("%s/sessions/%s.lua", scCache, leaf);
4359
4360 qhashtbl_t *tnm = qhashtbl(0, 0);
4361 ret = LuaToHash(Rd, t0, "toke_n_munchie", tnm, ret, &st, &now, "session");
4362 free(t0);
4363
4364 if (0 != ret)
4365 {
4366 // This might be coz it's a stale session that was deleted already, so shouldn't complain really if they are just getting the login page.
4367 // They might also have a stale doit and form cookie.
4368// bitchSession(Rd, "Invalid session.", "No session file.");
4369 bitchSession(Rd, "", "No session file.");
4370 ret++;
4371 }
4372 else
4373 {
4374 // This is apparently controversial, I added it coz some of the various security docs suggested it's a good idea.
4375 // https://security.stackexchange.com/questions/139952/why-arent-sessions-exclusive-to-an-ip-address?rq=1
4376 // Includes various reasons why it's bad.
4377 // Another good reason why it is bad, TOR.
4378 // So should make this a user option, like Mantis does.
4379 if (strcmp(getStrH(Rd->headers, "REMOTE_ADDR"), getStrH(tnm, "IP")) != 0)
4380 {
4381 bitchSession(Rd, "Wrong IP for session.", "Session IP doesn't match.");
4382 ret++;
4383 }
4384 else
4385 {
4386 timeStamp = xmprintf("%ld.%ld", (long) st.st_mtim.tv_sec, st.st_mtim.tv_nsec);
4387//d("timeStamp %s", timeStamp);
4388 seshion = xmprintf("%s%s", tnm->getstr(tnm, "seshID", false), timeStamp);
4389//d("sesh %s", seshion);
4390 t0 = myHMAC(seshion, FALSE);
4391 munchie = xmprintf("%s%s", t0, timeStamp);
4392//d("munchie %s", munchie);
4393 free(t0);
4394 free(timeStamp);
4395 t1 = getStrH(Rd->body, "munchie");
4396 if ('\0' != t1[0])
4397 {
4398 if (strcmp(t1, munchie) != 0)
4399 {
4400// TODO if newbie user has not logged out, but clicks the email linky, and they end up on a new browser tab, they'll see this on the logged in tab.
4401 bitchSession(Rd, "Wrong munchie for session, may have been eaten, please try again.", "HMAC(seshID + timeStamp) != munchie");
4402 ret++;
4403 }
4404 else
4405 {
4406 t0 = xmprintf("%s%s", getStrH(tnm, "UUID"), munchie);
4407 t1 = myHMAC(t0, FALSE);
4408 free(t0);
4409
4410//d("toke_n_munchie %s", t1);
4411 if (strcmp(t1, toke_n_munchie) != 0)
4412 {
4413 bitchSession(Rd, "Wrong toke_n_munchie for session.", "HMAC(UUID + munchie) != toke_n_munchie");
4414 ret++;
4415 }
4416 free(t1);
4417 }
4418
4419 if (linky)
4420 {
4421 t0 = xmprintf("%s%s", getStrH(tnm, "UUID"), munchie);
4422 t1 = myHMAC(t0, FALSE);
4423 free(t0);
4424 toke_n_munchie = t1;
4425//d("toke_n_munchie %s", t1);
4426 }
4427 t1 = myHMACkey(getStrH(tnm, "salt"), toke_n_munchie, FALSE);
4428//d("hashish %s", t1);
4429 if (strcmp(t1, hashish) != 0)
4430 {
4431 bitchSession(Rd, "Wrong hashish for session.", "HMAC(toke_n_munchie + salt) != hashish");
4432 ret++;
4433 }
4434 free(t1);
4435 }
4436
4437// TODO - should carefully review all of this, especially the moving of session data to and fro.
4438 if (0 == ret)
4439 {
4440W("Validated session.");
4441 sesh *shs = &Rd->shs;
4442
4443 qstrcpy(shs->leaf, sizeof(shs->leaf), leaf);
4444 if (NULL != shs->name) free(shs->name);
4445 shs->name = tnm->getstr(tnm, "name", true);
4446 if (NULL != shs->UUID) free(shs->UUID);
4447 shs->UUID = tnm->getstr(tnm, "UUID", true);
4448 if (linky)
4449 {
4450W("Validated session linky.");
4451 addStrL(Rd->messages, "Congratulations, you have validated your new account. Now you can log onto the web site.");
4452 addStrL(Rd->messages, "NOTE - you wont be able to log onto the grid until your new account has been approved.");
4453 Rd->lnk = xzalloc(sizeof(sesh));
4454 Rd->lnk->status = SHS_NUKE;
4455 qstrcpy(Rd->lnk->leaf, sizeof(Rd->lnk->leaf), leaf);
4456 freeSesh(Rd, linky, FALSE);
4457 qstrcpy(Rd->lnk->leaf, sizeof(Rd->lnk->leaf), "");
4458 Rd->doit = "validate";
4459 Rd->output = "accountLogin";
4460 Rd->form = "accountLogin";
4461// TODO - we might want to delete their old .lua session as well. Maybe? Don't think we have any suitable codes to find it.
4462 }
4463 else
4464 {
4465 char *level = tnm->getstr(tnm, "level", false);
4466
4467 // Check for session timeouts etc.
4468 if (now.tv_sec > st.st_mtim.tv_sec + seshTimeOut)
4469 {
4470 bitch(Rd, "Session timed out.", "No activity for longer than seshTimeOut, session is ancient.");
4471 ret++;
4472 Rd->shs.status = SHS_ANCIENT;
4473 }
4474 else if (now.tv_sec > st.st_mtim.tv_sec + idleTimeOut)
4475 {
4476 bitch(Rd, "Session idled out.", "No activity for longer than idleTimeOut, session is idle.");
4477 ret++;
4478 Rd->shs.status = SHS_IDLE;
4479 }
4480 else if (now.tv_sec > st.st_mtim.tv_sec + seshRenew)
4481 {
4482 D("Session needs renewing.");
4483 Rd->shs.status = SHS_RENEW;
4484 }
4485 else
4486 Rd->shs.status = SHS_VALID;
4487
4488 if (NULL == level)
4489 level = "-256";
4490 qstrcpy(shs->sesh, sizeof(shs->sesh), seshion);
4491 qstrcpy(shs->toke_n_munchie, sizeof(shs->toke_n_munchie), toke_n_munchie);
4492 qstrcpy(shs->hashish, sizeof(shs->hashish), hashish);
4493 qstrcpy(shs->munchie, sizeof(shs->munchie), munchie);
4494 qstrcpy(shs->salt, sizeof(shs->salt), tnm->getstr(tnm, "salt", false));
4495 qstrcpy(shs->seshID, sizeof(shs->seshID), tnm->getstr(tnm, "seshID", false));
4496 shs->level = atoi(level);
4497// TODO - get level from somewhere and stuff it in shs.
4498 shs->timeStamp[0].tv_nsec = UTIME_OMIT;
4499 shs->timeStamp[0].tv_sec = UTIME_OMIT;
4500 memcpy(&shs->timeStamp[1], &st.st_mtim, sizeof(struct timespec));
4501 }
4502 }
4503
4504 qhashtbl_obj_t obj;
4505
4506 memset((void*)&obj, 0, sizeof(obj));
4507 tnm->lock(tnm);
4508 while(tnm->getnext(tnm, &obj, false) == true)
4509 {
4510 char *n = obj.name;
4511
4512 if ((strcmp("salt", n) != 0) && (strcmp("seshID", n) != 0) && (strcmp("UUID", n) != 0))
4513 {
4514t("SessionValidate() Lua read %s = %s", n, (char *) obj.data);
4515 Rd->stuff->putstr(Rd->stuff, obj.name, (char *) obj.data);
4516 }
4517 }
4518 tnm->unlock(tnm);
4519
4520// TODO - check this.
4521// Rd->database->putstr(Rd->database, "UserAccounts.PrincipalID", tnm->getstr(tnm, "UUID", false));
4522 }
4523 free(munchie);
4524 free(seshion);
4525 }
4526 free(leaf);
4527 tnm->free(tnm);
4528 free(hashish);
4529 }
4530
4531 return ret;
4532}
4533
4534static void sessionWeb(reqData *Rd, inputForm *iF, inputValue *iV)
4535{
4536 HTMLhidden(Rd->reply, iV->field->name, iV->value);
4537}
4538
4539/*
4540static int UUIDValidate(reqData *Rd, inputForm *iF, inputValue *iV)
4541{
4542 int ret = 0;
4543 char *UUID = (char *) iV->value;
4544
4545 if (36 != strlen(UUID))
4546 {
4547 bitch(Rd, "Internal error.", "UUID isn't long enough.");
4548 ret++;
4549 }
4550// TODO - check the characters and dashes as well.
4551
4552 if (0 == ret)
4553 Rd->stuff->putstr(Rd->stuff, "UUID", UUID);
4554 return ret;
4555}
4556
4557static void UUIDWeb(reqData *Rd, inputForm *iF, inputValue *iV)
4558{
4559 HTMLhidden(Rd->reply, iV->field->name, iV->value);
4560}
4561*/
4562
4563static int nameValidate(reqData *Rd, inputForm *iF, inputValue *iV)
4564{
4565 int ret = 0;
4566 unsigned char *name; // We have to be unsigned coz of isalnum().
4567 char *where = NULL;
4568
4569 name = xstrdup(iV->value);
4570
4571 if ((NULL == name) || ('\0' == name[0]))
4572 {
4573 bitch(Rd, "Please supply an account name.", "None supplied.");
4574 ret++;
4575 }
4576 else
4577 {
4578 int l0 = strlen(name), l1 = 0, l2 = 0;
4579
4580 if (0 == l0)
4581 {
4582 bitch(Rd, "Please supply an account name.", "Name is empty.");
4583 ret++;
4584 }
4585 else
4586 {
4587 int i;
4588 unsigned char *s = NULL;
4589
4590 for (i = 0; i < l0; i++)
4591 {
4592 if (isalnum(name[i]) == 0)
4593 {
4594
4595 if ((' ' == name[i] /*&& (NULL == s)*/))
4596 {
4597 s = &name[i];
4598 *s++ = '\0';
4599 while(' ' == *s)
4600 {
4601 i++;
4602 s++;
4603 }
4604 l1 = strlen(name);
4605 l2 = strlen(s);
4606
4607 // Apparently names are not case sensitive on login, but stored with case in the database.
4608 // I confirmed that, can log in no matter what case you use.
4609 // Seems to be good security for names to be case insensitive.
4610 // UserAccounts FirstName and LastName fields are both varchar(64) utf8_general_ci.
4611 // The MySQL docs say that the "_ci" bit means comparisons will be case insensitive. So that should work fine.
4612
4613 // SL docs say 31 characters each for first and last name. UserAccounts table is varchar(64) each. userinfo has varchar(50) for the combined name.
4614 // The userinfo table seems to be obsolete.
4615 // Singularity at least limits the total name to 64.
4616 // I can't find any limitations on characters allowed, but I only ever see letters and digits used. Case is stored, but not significant.
4617 // OpenSims "create user" console command doesn't sanitize it at all, even crashing on some names.
4618 }
4619 else
4620 {
4621 bitch(Rd, "First and last names are limited to ordinary letters and digits, no special characters or fonts.", "");
4622 ret++;
4623 break;
4624 }
4625// TODO - compare first, last, and fullname against god names, complain and fail if there's a match.
4626 }
4627 }
4628
4629 if (NULL == s)
4630 {
4631 bitch(Rd, "Account names have to be two words.", "");
4632 ret++;
4633 }
4634 if ((31 < l1) || (31 < l2))
4635 {
4636 bitch(Rd, "First and last names are limited to 31 letters each.", "");
4637 ret++;
4638 }
4639 if ((0 == l1) || (0 == l2))
4640 {
4641 bitch(Rd, "First and last names have to be one or more ordinary letters or digits each.", "");
4642 ret++;
4643 }
4644
4645 if (0 == ret)
4646 {
4647 Rd->stuff->putstr(Rd->stuff, "firstName", name);
4648 Rd->stuff->putstr(Rd->stuff, "lastName", s);
4649 Rd->stuff->putstrf(Rd->stuff, "name", "%s %s", name, s);
4650 }
4651 }
4652 }
4653 free(name);
4654
4655 return ret;
4656}
4657
4658static void nameWeb(reqData *Rd, inputForm *oF, inputValue *oV)
4659{
4660 if (oV->field->flags & FLD_HIDDEN)
4661 HTMLhidden(Rd->reply, oV->field->name, oV->value);
4662 else
4663 HTMLtext(Rd->reply, "text", oV->field->title, oV->field->name, oV->value, oV->field->viewLength, oV->field->maxLength, oV->field->flags & FLD_REQUIRED);
4664}
4665
4666
4667static int passwordValidate(reqData *Rd, inputForm *iF, inputValue *iV)
4668{
4669 int ret = 0;
4670 char *password = (char *) iV->value, *salt = getStrH(Rd->stuff, "passSalt"), *hash = getStrH(Rd->stuff, "passHash");
4671
4672 if ((NULL == password) || ('\0' == password[0]))
4673 {
4674 bitch(Rd, "Please supply a password.", "Password empty or missing.");
4675 ret++;
4676 }
4677 else if (('\0' != salt[0]) && ('\0' != hash[0]) && (strcmp("psswrd", iV->field->name) == 0))
4678 {
4679 D("Comparing passwords. %s %s %s", password, salt, hash);
4680 char *h = checkSLOSpassword(Rd, salt, password, hash, "Passwords are not the same.");
4681
4682 if (NULL == h)
4683 ret++;
4684 else
4685 free(h);
4686 }
4687
4688// TODO - once the password is validated, store it as the salt and hash.
4689// If it's an existing account, compare it? Or do that later?
4690 if (0 == ret)
4691 Rd->stuff->putstr(Rd->stuff, "password", password);
4692
4693 return ret;
4694}
4695
4696static void passwordWeb(reqData *Rd, inputForm *oF, inputValue *oV)
4697{
4698 HTMLtext(Rd->reply, "password", oV->field->title, oV->field->name, "", oV->field->viewLength, oV->field->maxLength, oV->field->flags & FLD_REQUIRED);
4699 Rd->reply->addstr(Rd->reply, "<p>While viewers will usually remember your name and password for you, you'll need to remember it for this web site to. &nbsp; "
4700 "I highly recommend using a password manager. &nbsp; KeePass and it's variations is a great password manager.</p>\n");
4701}
4702
4703static int emailValidate(reqData *Rd, inputForm *iF, inputValue *iV)
4704{
4705// inputField **group = iV->field->group;
4706 int ret = 0, i;
4707 boolean notSame = FALSE;
4708
4709 i = iV->index;
4710 if (2 == i)
4711 {
4712 char *email = (char *) iV->value;
4713 char *emayl = (char *) (iV + 1)->value;
4714
4715 if ((NULL == email) || (NULL == emayl) || ('\0' == email[0]) || ('\0' == emayl[0]))
4716 {
4717 bitch(Rd, "Please supply an email address.", "None supplied.");
4718 ret++;
4719 }
4720 else if (strcmp(email, emayl) != 0)
4721 {
4722 bitch(Rd, "Email addresses are not the same.", "");
4723 ret++;
4724 notSame = TRUE;
4725 }
4726 else if (!qstr_is_email(email))
4727 {
4728 bitch(Rd, "Please supply a proper email address.", "Failed qstr_is_email()");
4729 ret++;
4730 }
4731 else
4732 {
4733// TODO - do other email checks - does the domain exist, ..
4734 }
4735
4736 if ((NULL != email) && (NULL != emayl))
4737 {
4738 char *t0 = qurl_encode(email, strlen(email));
4739
4740 // In theory it's the correct thing to do to NOT load email into stuff on failure,
4741 // In practice, that means it wont show the old email and emayl in the create page when they don't match.
4742 if ((0 == ret) || notSame)
4743 Rd->stuff->putstrf(Rd->stuff, "email", "%s", t0);
4744 free(t0);
4745 }
4746 if ((NULL != email) && (NULL != emayl))
4747 {
4748 char *t1 = qurl_encode(emayl, strlen(emayl));
4749
4750 Rd->stuff->putstrf(Rd->stuff, "emayl", "%s", t1);
4751 free(t1);
4752 }
4753 }
4754
4755 return ret;
4756}
4757static void emailWeb(reqData *Rd, inputForm *oF, inputValue *oV)
4758{
4759 HTMLtext(Rd->reply, "email", oV->field->title, oV->field->name, getStrH(Rd->stuff, oV->field->name), oV->field->viewLength, oV->field->maxLength, oV->field->flags & FLD_REQUIRED);
4760 Rd->reply->addstrf(Rd->reply, "<p>An email will be sent from %s@%s, and it might be in your spam folder, coz these sorts of emails sometimes end up there. &nbsp; "
4761 "You should add that email address to your contacts, or otherwise let it through your spam filter.</p>",
4762 "grid_no_reply", Rd->Host);
4763}
4764
4765
4766char *months[] =
4767{
4768 "january",
4769 "february",
4770 "march",
4771 "april",
4772 "may",
4773 "june",
4774 "july",
4775 "august",
4776 "september",
4777 "october",
4778 "november",
4779 "december"
4780};
4781static int DoBValidate(reqData *Rd, inputForm *iF, inputValue *iV)
4782{
4783 int ret = 0, i;
4784 char *t0, *t1;
4785// inputField **group = iV->field->group;
4786
4787 i = iV->index;
4788 if (2 == i)
4789 {
4790 t0 = (char *) iV->value;
4791 if ((NULL == t0) || ('\0' == t0[0]))
4792 {
4793 bitch(Rd, "Please supply a year of birth.", "None supplied.");
4794 ret++;
4795 }
4796 else
4797 {
4798 i = atoi(t0);
4799// TODO - get this to use current year instead of 2020.
4800 if ((1900 > i) || (i > 2020))
4801 {
4802 bitch(Rd, "Please supply a year of birth.", "Out of range.");
4803 ret++;
4804 }
4805 else if (i < 1901)
4806 {
4807 bitch(Rd, "Please supply a proper year of birth.", "Out of range, too old.");
4808 ret++;
4809 }
4810 else if (i >2004)
4811 {
4812 bitch(Rd, "This grid is Adult rated, you are too young.", "Out of range, too young.");
4813 ret++;
4814 }
4815 }
4816 t1 = (char *) (iV + 1)->value;
4817 if ((NULL == t1) || ('\0' == t1[0]))
4818 {
4819 bitch(Rd, "Please supply a month of birth.", "None supplied.");
4820 ret++;
4821 }
4822 else
4823 {
4824 for (i = 0; i < 12; i++)
4825 {
4826 if (strcmp(months[i], t1) == 0)
4827 break;
4828 }
4829 if (12 == i)
4830 {
4831 bitch(Rd, "Please supply a month of birth.", "Out of range");
4832 ret++;
4833 }
4834 }
4835
4836 if (0 == ret)
4837 {
4838 Rd->stuff->putstr(Rd->stuff, "year", t0);
4839 Rd->stuff->putstr(Rd->stuff, "month", t1);
4840 Rd->stuff->putstrf(Rd->stuff, "DoB", "%s %s", t0, t1);
4841 }
4842 }
4843
4844 return ret;
4845}
4846static void DoByWeb(reqData *Rd, inputForm *oF, inputValue *oV)
4847{
4848 char *tmp = xmalloc(16), *t;
4849 int i, d;
4850
4851 Rd->reply->addstr(Rd->reply, "<label>Date of birth :<table><tr><td>\n");
4852 HTMLselect(Rd->reply, NULL, oV->field->name);
4853 t = getStrH(Rd->stuff, "year");
4854 if (NULL == t)
4855 d = -1;
4856 else
4857 d = atoi(t);
4858 HTMLoption(Rd->reply, "", FALSE);
4859 for (i = 1900; i <= 2020; i++)
4860 {
4861 boolean sel = FALSE;
4862
4863 if (i == d)
4864 sel = TRUE;
4865 sprintf(tmp, "%d", i);
4866 HTMLoption(Rd->reply, tmp, sel);
4867 }
4868 free(tmp);
4869 HTMLselectEndNo(Rd->reply);
4870}
4871static void DoBmWeb(reqData *Rd, inputForm *oF, inputValue *oV)
4872{
4873 char *t;
4874 int i, d;
4875
4876 Rd->reply->addstr(Rd->reply, "</td><td>\n");
4877 HTMLselect(Rd->reply, NULL, oV->field->name);
4878 t = getStrH(Rd->stuff, "month");
4879 HTMLoption(Rd->reply, "", FALSE);
4880 for (i = 0; i <= 11; i++)
4881 {
4882 boolean sel = FALSE;
4883
4884 if ((NULL != t) && (strcmp(t, months[i]) == 0))
4885 sel = TRUE;
4886 HTMLoption(Rd->reply, months[i], sel);
4887 }
4888 HTMLselectEndNo(Rd->reply);
4889 Rd->reply->addstr(Rd->reply, "</td></tr></table></label>\n");
4890}
4891static void DoBWeb(reqData *Rd, inputForm *oF, inputValue *oV)
4892{
4893}
4894
4895static int legalValidate(reqData *Rd, inputForm *iF, inputValue *iV)
4896{
4897 int ret = 0, i;
4898 char *t;
4899 inputField **group = iV->field->group;
4900
4901 i = iV->index;
4902 if (2 == i)
4903 {
4904 t = (char *) iV->value;
4905 if ((NULL == t) || (strcmp("on", t) != 0))
4906 {
4907 bitch(Rd, "You must be an adult to enter this site.", "");
4908 ret++;
4909 }
4910 else
4911 Rd->stuff->putstr(Rd->stuff, "adult", t);
4912 t = (char *) (iV + 1)->value;
4913 if ((NULL == t) || (strcmp("on", t) != 0))
4914 {
4915 bitch(Rd, "You must agree to the Terms & Conditions of Use.", "");
4916 ret++;
4917 }
4918 else
4919 Rd->stuff->putstr(Rd->stuff, "agree", t);
4920 }
4921
4922 return ret;
4923}
4924static void adultWeb(reqData *Rd, inputForm *oF, inputValue *oV)
4925{
4926 HTMLcheckBox(Rd->reply, oV->field->name, oV->field->title, !strcmp("on", getStrH(Rd->body, "adult")), oV->field->flags & FLD_REQUIRED);
4927}
4928static void agreeWeb(reqData *Rd, inputForm *oF, inputValue *oV)
4929{
4930 HTMLcheckBox(Rd->reply, oV->field->name, oV->field->title, !strcmp("on", getStrH(Rd->body, "agree")), oV->field->flags & FLD_REQUIRED);
4931}
4932static void legalWeb(reqData *Rd, inputForm *oF, inputValue *oV)
4933{
4934}
4935static void ToSWeb(reqData *Rd, inputForm *oF, inputValue *oV)
4936{
4937 Rd->reply->addstrf(Rd->reply, "<h2>Terms of Service</h2><pre>%s</pre>\n", getStrH(Rd->configs, "ToS"));
4938}
4939
4940static int voucherValidate(reqData *Rd, inputForm *oF, inputValue *oV)
4941{
4942 int ret = 0;
4943 char *voucher = (char *) oV->value;
4944
4945 if ((NULL == voucher) || ('\0' == voucher[0]))
4946 {
4947 bitch(Rd, "Please fill in the 'Voucher' section.", "None supplied.");
4948 ret++;
4949 }
4950
4951 if ((0 == ret) && (NULL != voucher))
4952 Rd->stuff->putstr(Rd->stuff, "voucher", voucher);
4953
4954 return ret;
4955}
4956static void voucherWeb(reqData *Rd, inputForm *oF, inputValue *oV)
4957{
4958 HTMLtext(Rd->reply, "text", oV->field->title, oV->field->name, oV->value, oV->field->viewLength, oV->field->maxLength, oV->field->flags & FLD_REQUIRED);
4959}
4960
4961static int aboutMeValidate(reqData *Rd, inputForm *oF, inputValue *oV)
4962{
4963 int ret = 0;
4964 char *about = (char *) oV->value;
4965
4966 if ((NULL == about) || ('\0' == about[0]))
4967 {
4968 bitch(Rd, "Please fill in the 'About me' section.", "None supplied.");
4969 ret++;
4970 }
4971
4972 if ((0 == ret) && (NULL != about))
4973 Rd->stuff->putstr(Rd->stuff, "aboutMe", about);
4974
4975 return ret;
4976}
4977
4978static void aboutMeWeb(reqData *Rd, inputForm *oF, inputValue *oV)
4979{
4980 // 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.
4981 // 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
4982// TODO - check against the limit for in world profiles, coz this will become that.
4983// TODO - validate aboutMe, it should not be empty, and should not be longer than 64 kilobytes.
4984 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);
4985}
4986
4987static void accountWebHeaders(reqData *Rd, inputForm *oF) //, char *name)
4988{
4989 char *linky = checkLinky(Rd);
4990
4991 HTMLheader(Rd->reply, "<!--#echo var=\"grid\" --> account manager");
4992 Rd->reply->addstrf(Rd->reply, "<h1><!--#echo var=\"grid\" --> account manager</h1>\n");
4993 if (NULL != Rd->shs.name)
4994 {
4995 char *nm = qstrreplace("tr", xstrdup(Rd->shs.name), " ", "+");
4996
4997 Rd->reply->addstrf(Rd->reply, "<h3>You are <a href='https://%s%s%s?user=%s'>%s</a></h3>\n", Rd->Host, Rd->Script, Rd->Path, nm, Rd->shs.name);
4998 Rd->reply->addstr(Rd->reply, linky);
4999 free(nm);
5000 }
5001 free(linky);
5002 if (0 != Rd->errors->size(Rd->messages))
5003 HTMLlist(Rd->reply, "messages -", Rd->messages);
5004 if (NULL != oF->help)
5005 Rd->reply->addstrf(Rd->reply, "<p>%s</p>\n", oF->help);
5006 HTMLform(Rd->reply, "", Rd->shs.munchie);
5007 HTMLhidden(Rd->reply, "form", oF->name);
5008}
5009
5010static void accountWebFields(reqData *Rd, inputForm *oF, inputValue *oV)
5011{
5012 int count = oF->fields->size(oF->fields), i;
5013
5014 for (i = 0; i < count; i++)
5015 {
5016 if (NULL != oV[i].field->web)
5017 oV[i].field->web(Rd, oF, &oV[i]);
5018 if ((NULL != oV[i].field->help) && ('\0' != oV[i].field->help[0]))
5019 Rd->reply->addstrf(Rd->reply, "<p>%s</p>\n", oV[i].field->help);
5020//d("accountWebFeilds(%s, %s)", oF->name, oV[i].field->name);
5021 }
5022}
5023
5024static void accountWebSubs(reqData *Rd, inputForm *oF)
5025{
5026 qhashtbl_obj_t obj;
5027
5028 Rd->reply->addstrf(Rd->reply, "<input type='submit' disabled style='display: none' aria-hidden='true' />\n"); // Stop Enter key on text fields triggering the first submit button.
5029 memset((void*)&obj, 0, sizeof(obj)); // must be cleared before call
5030 oF->subs->lock(oF->subs);
5031 while(oF->subs->getnext(oF->subs, &obj, false) == true)
5032 {
5033 inputSub *sub = (inputSub *) obj.data;
5034 if ('\0' != sub->title[0])
5035 HTMLbutton(Rd->reply, sub->name, sub->title);
5036//d("accountWebSubs(%s, %s '%s')", oF->name, sub->name, sub->title);
5037 }
5038 oF->subs->unlock(oF->subs);
5039}
5040
5041static void accountWebFooter(reqData *Rd, inputForm *oF)
5042{
5043 if (0 != Rd->errors->size(Rd->errors))
5044 HTMLlist(Rd->reply, "errors -", Rd->errors);
5045 HTMLformEnd(Rd->reply);
5046 HTMLfooter(Rd->reply);
5047}
5048
5049static void accountAddWeb(reqData *Rd, inputForm *oF, inputValue *oV)
5050{
5051 accountWebHeaders(Rd, oF);
5052 accountWebFields(Rd, oF, oV);
5053 accountWebSubs(Rd, oF);
5054 accountWebFooter(Rd, oF);
5055}
5056
5057static void accountLoginWeb(reqData *Rd, inputForm *oF, inputValue *oV)
5058{
5059 if (NULL != Rd->shs.name) free(Rd->shs.name);
5060 Rd->shs.name = NULL;
5061 if (NULL != Rd->shs.UUID) free(Rd->shs.UUID);
5062 Rd->shs.UUID = NULL;
5063 accountWebHeaders(Rd, oF);
5064 accountWebFields(Rd, oF, oV);
5065 accountWebSubs(Rd, oF);
5066 accountWebFooter(Rd, oF);
5067}
5068
5069// TODO - accountViewWeb() and accountViewWeb() should view and edit arbitrary accounts the user is not logged in as,
5070// but limit things based on being that viewed / edited account, and the users level.
5071static void accountViewWeb(reqData *Rd, inputForm *oF, inputValue *oV)
5072{
5073 char *name = getStrH(Rd->database, "Lua.name"),
5074 *level = getStrH(Rd->database, "UserAccounts.UserLevel"),
5075 *email = getStrH(Rd->database, "UserAccounts.Email"),
5076 *voucher = getStrH(Rd->database, "Lua.voucher"),
5077 *about = getStrH(Rd->database, "Lua.aboutMe");
5078 time_t crtd = atol(getStrH(Rd->database, "UserAccounts.Created"));
5079
5080 accountWebHeaders(Rd, oF);
5081 accountWebFields(Rd, oF, oV);
5082 Rd->reply->addstrf(Rd->reply, "<p><font size='5'><span style='font-size: x-large'><b>Name :</b></span></font> %s</p>", name);
5083 Rd->reply->addstrf(Rd->reply, "<p><font size='5'><span style='font-size: x-large'><b>Title / level :</b></span></font> %s / %s</p>", getLevel(atoi(level)), level);
5084 Rd->reply->addstrf(Rd->reply, "<p><font size='5'><span style='font-size: x-large'><b>Date of birth :</b></span></font> %s</p>", getStrH(Rd->database, "Lua.DoB"));
5085 Rd->reply->addstrf(Rd->reply, "<p><font size='5'><span style='font-size: x-large'><b>Created :</b></span></font> %s</p>", ctime(&crtd));
5086 Rd->reply->addstrf(Rd->reply, "<p><font size='5'><span style='font-size: x-large'><b>Email :</b></span></font> %s</p>", email);
5087 Rd->reply->addstrf(Rd->reply, "<p><font size='5'><span style='font-size: x-large'><b>UUID :</b></span></font> %s</p>", getStrH(Rd->database, "UserAccounts.PrincipalID"));
5088 Rd->reply->addstrf(Rd->reply, "<p><font size='5'><span style='font-size: x-large'><b>Voucher :</b></span></font> %s</p>", voucher);
5089 HTMLtextArea(Rd->reply, "aboutMe", "About", 7, 50, 4, 16384, "", "off", "true", "soft", about, FALSE, TRUE);
5090 accountWebSubs(Rd, oF);
5091 accountWebFooter(Rd, oF);
5092}
5093
5094static void accountEditWeb(reqData *Rd, inputForm *oF, inputValue *oV)
5095{
5096 char *name = getStrH(Rd->database, "Lua.name"),
5097 *level = getStrH(Rd->database, "UserAccounts.UserLevel"),
5098 *email = getStrH(Rd->database, "UserAccounts.Email"),
5099 *voucher = getStrH(Rd->database, "Lua.voucher"),
5100 *about = getStrH(Rd->database, "Lua.aboutMe"),
5101 *lvl = getLevel(atoi(level));
5102 short lv = atoi(level);
5103
5104 accountWebHeaders(Rd, oF);
5105 accountWebFields(Rd, oF, oV);
5106// HTMLtext(Rd->reply, "password", "Old password", "password", "", 16, 0, FALSE);
5107// Rd->reply->addstr(Rd->reply, "<p>Warning, the limit on password length is set by your viewer, some can't handle longer than 16 characters.</p>\n");
5108//// HTMLtext(Rd->reply, "title", "text", "title", getStrH(Rh->stuff, "title"), 16, 64, TRUE);
5109
5110 HTMLhidden(Rd->reply, "user", name);
5111 Rd->reply->addstrf(Rd->reply, "<p><font size='5'><span style='font-size: x-large'><b>Name :</b></span></font> %s</p>", name);
5112// Rd->reply->addstrf(Rd->reply, "<p><font size='5'><span style='font-size: x-large'><b>Email :</b></span></font> %s</p>", email);
5113 HTMLtextArea(Rd->reply, "aboutMe", "About", 7, 50, 4, 16384, "", "off", "true", "soft", about, FALSE, TRUE);
5114 Rd->reply->addstrf(Rd->reply, "<p><font size='5'><span style='font-size: x-large'><b>Voucher :</b></span></font> %s</p>", voucher);
5115
5116 if (200 <= Rd->shs.level)
5117 {
5118 qlisttbl_obj_t obj;
5119
5120 HTMLselect(Rd->reply, "level", "level");
5121 memset((void*)&obj, 0, sizeof(obj)); // must be cleared before call
5122 accountLevels->lock(accountLevels);
5123 while(accountLevels->getnext(accountLevels, &obj, NULL, false) == true)
5124 {
5125 boolean is = false;
5126 short l = atoi((char *) obj.name);
5127
5128 if (strcmp(lvl, (char *) obj.data) == 0)
5129 is = true;
5130
5131// if ((is) || ((l <= Rd->shs.level) && (l != -200) && (l != -100) && (l != -50))) // Not above our pay grade, not newbie, validated, nor vouched for.
5132 if ((is) || ((l <= Rd->shs.level) && (lv <= l))) // As per discussions, can't lower level. Do that in the console.
5133 HTMLoption(Rd->reply, (char *) obj.data, is);
5134 }
5135 accountLevels->unlock(accountLevels);
5136 HTMLselectEnd(Rd->reply);
5137
5138 Rd->reply->addstrf(Rd->reply, "<p><dl>");
5139 Rd->reply->addstrf(Rd->reply, "<dt>disabled</dt><dd>Account cannot log in anywhere.</dd>");
5140 Rd->reply->addstrf(Rd->reply, "<dt>newbie</dt><dd>Newly created account, not yet validated.</dd>");
5141 Rd->reply->addstrf(Rd->reply, "<dt>validated</dt><dd>Newly created account, they have clicked on the validation link in their validation email.</dd>");
5142 Rd->reply->addstrf(Rd->reply, "<dt>vouched for</dt><dd>Someone has vouched for this person.</dd>");
5143 Rd->reply->addstrf(Rd->reply, "<dt>approved</dt><dd>This person is approved, and can log into the world.</dd>");
5144 Rd->reply->addstrf(Rd->reply, "<dt>god</dt><dd>This is a god admin person.</dd>");
5145 Rd->reply->addstrf(Rd->reply, "</dl></p>");
5146 }
5147 else
5148 Rd->reply->addstrf(Rd->reply, "<p><font size='5'><span style='font-size: x-large'><b>Title / level :</b></span></font> %s / %s</p>", lvl, level);
5149
5150 accountWebSubs(Rd, oF);
5151 accountWebFooter(Rd, oF);
5152}
5153
5154
5155static int accountRead(reqData *Rd, char *uuid, char *firstName, char *lastName)
5156{
5157 int ret = 0, rt = -1;
5158 struct stat st;
5159 struct timespec now;
5160 qhashtbl_t *tnm = qhashtbl(0, 0);
5161 uuid_t binuuid;
5162 rowData *rows = NULL;
5163
5164 // Setup the database stuff.
5165 static dbRequest *uuids = NULL;
5166 if (NULL == uuids)
5167 {
5168 static char *szi[] = {"PrincipalID", NULL};
5169 static char *szo[] = {NULL};
5170 uuids = xzalloc(sizeof(dbRequest));
5171 uuids->table = "UserAccounts";
5172 uuids->inParams = szi;
5173 uuids->outParams = szo;
5174 uuids->where = "PrincipalID=?";
5175 dbRequests->addfirst(dbRequests, &uuids, sizeof(dbRequest *));
5176 }
5177 static dbRequest *acnts = NULL;
5178 if (NULL == acnts)
5179 {
5180 static char *szi[] = {"FirstName", "LastName", NULL};
5181 static char *szo[] = {NULL};
5182 acnts = xzalloc(sizeof(dbRequest));
5183 acnts->table = "UserAccounts";
5184 acnts->inParams = szi;
5185 acnts->outParams = szo;
5186 acnts->where = "FirstName=? and LastName=?";
5187 dbRequests->addfirst(dbRequests, &acnts, sizeof(dbRequest *));
5188 }
5189 static dbRequest *auth = NULL;
5190 if (NULL == auth)
5191 {
5192 static char *szi[] = {"UUID", NULL};
5193 static char *szo[] = {"passwordSalt", "passwordHash", NULL};
5194 auth = xzalloc(sizeof(dbRequest));
5195 auth->table = "auth";
5196 auth->inParams = szi;
5197 auth->outParams = szo;
5198 auth->where = "UUID=?";
5199 dbRequests->addfirst(dbRequests, &auth, sizeof(dbRequest *));
5200 }
5201
5202 Rd->fromDb = FALSE;
5203
5204// uuid = Rd->shs.UUID; first = getStrH(Rd->stuff, "firstName"); last = getStrH(Rd->stuff, "lastName");
5205
5206 // Special for showing another users details.
5207 if ('\0' != getStrH(Rd->queries, "user")[0])
5208 uuid = "";
5209
5210 char *first = xstrdup(""), *last = xstrdup("");
5211
5212 if (NULL != firstName)
5213 {
5214 free(first);
5215 first = xstrdup(firstName);
5216 if (NULL == lastName)
5217 {
5218 char *t = strchr(first, ' ');
5219
5220d("accountRead() single name |%s| |%s|", first, last);
5221 if (NULL == t)
5222 t = strchr(first, '+');
5223 if (NULL != t)
5224 {
5225 *t++ = '\0';
5226 free(last);
5227 last = xstrdup(t);
5228 }
5229 }
5230 else
5231 {
5232 free(last);
5233 last = xstrdup(lastName);
5234 }
5235 }
5236d("accountRead() UUID %s, name %s %s", uuid, first, last);
5237 uuid_clear(binuuid);
5238 if ((NULL != uuid) && ('\0' != uuid[0]))
5239 uuid_parse(uuid, binuuid);
5240 if ((NULL != uuid) && ('\0' != uuid[0]) && (!uuid_is_null(binuuid)))
5241 {
5242 char *where = xmprintf("%s/users/%s.lua", scData, uuid);
5243 rt = LuaToHash(Rd, where, "user", tnm, ret, &st, &now, "user");
5244
5245 free(where);
5246 dbDoSomething(uuids, FALSE, uuid);
5247 rows = uuids->rows;
5248 }
5249 else
5250 {
5251
5252 if ('\0' != first[0])
5253 {
5254 char *where = xmprintf("%s/users/%s_%s.lua", scData, first, last);
5255 rt = LuaToHash(Rd, where, "user", tnm, ret, &st, &now, "user");
5256
5257 free(where);
5258 dbDoSomething(acnts, FALSE, first, last); // LEAKY
5259 rows = acnts->rows;
5260 }
5261 }
5262// else
5263// {
5264// bitch(Rd, "Unable to read user record.", "Nothing available to look up a user record with.");
5265// rt = 1;
5266// }
5267
5268 if (0 == rt)
5269 {
5270T("Found Lua record.");
5271 ret += 1;
5272 Rd->database->putstr(Rd->database, "UserAccounts.FirstName", first);
5273 Rd->database->putstr(Rd->database, "UserAccounts.LastName", last);
5274 Rd->database->putstr(Rd->database, "UserAccounts.Email", getStrH(tnm, "email"));
5275 Rd->database->putstr(Rd->database, "UserAccounts.Created", getStrH(tnm, "created"));
5276 Rd->database->putstr(Rd->database, "UserAccounts.PrincipalID", getStrH(tnm, "UUID"));
5277 Rd->database->putstr(Rd->database, "UserAccounts.UserLevel", getStrH(tnm, "level"));
5278 Rd->database->putstr(Rd->database, "UserAccounts.UserFlags", getStrH(tnm, "flags"));
5279 Rd->database->putstr(Rd->database, "UserAccounts.UserTitle", getStrH(tnm, "title"));
5280 Rd->database->putstr(Rd->database, "UserAccounts.active", getStrH(tnm, "active"));
5281 Rd->database->putstr(Rd->database, "auth.passwordSalt", getStrH(tnm, "passwordSalt"));
5282 Rd->database->putstr(Rd->database, "auth.passwordHash", getStrH(tnm, "passwordHash"));
5283 Rd->stuff-> putstr(Rd->stuff, "linky-hashish", getStrH(tnm, "linky-hashish"));
5284 Rd->database->putstr(Rd->database, "Lua.name", getStrH(tnm, "name"));
5285 Rd->database->putstr(Rd->database, "Lua.DoB", getStrH(tnm, "DoB"));
5286 Rd->database->putstr(Rd->database, "Lua.agree", getStrH(tnm, "agree"));
5287 Rd->database->putstr(Rd->database, "Lua.adult", getStrH(tnm, "adult"));
5288 Rd->database->putstr(Rd->database, "Lua.aboutMe", getStrH(tnm, "aboutMe"));
5289 Rd->database->putstr(Rd->database, "Lua.vouched", getStrH(tnm, "vouched"));
5290 Rd->database->putstr(Rd->database, "Lua.voucher", getStrH(tnm, "voucher"));
5291 }
5292// else if (rows)
5293 if (rows)
5294 {
5295 rt = rows->rows->size(rows->rows);
5296 if (1 == rt)
5297 {
5298 ret = rt;
5299T("Found database record.");
5300 dbPull(Rd, "UserAccounts", rows);
5301
5302 char *name = xmprintf("%s %s", getStrH(Rd->database, "UserAccounts.FirstName"), getStrH(Rd->database, "UserAccounts.LastName"));
5303
5304 Rd->fromDb = TRUE;
5305 Rd->database->putstr(Rd->database, "Lua.name", name);
5306 free(name);
5307 dbDoSomething(auth, FALSE, getStrH(Rd->database, "UserAccounts.PrincipalID")); // LEAKY
5308 rows = auth->rows;
5309 if (rows)
5310 {
5311 if (1 == rows->rows->size(rows->rows))
5312 dbPull(Rd, "auth", rows);
5313 else
5314 {
5315 free(rows->fieldNames);
5316 rows->rows->free(rows->rows);
5317 free(rows);
5318 }
5319 }
5320 else
5321 {
5322 free(rows->fieldNames);
5323 rows->rows->free(rows->rows);
5324 free(rows);
5325 }
5326 }
5327 else
5328 {
5329 free(rows->fieldNames);
5330 rows->rows->free(rows->rows);
5331 free(rows);
5332 }
5333 }
5334 else
5335 {
5336 d("No user name or UUID to get an account for.");
5337 }
5338
5339 if (1 == ret)
5340 {
5341// TODO - this has to change when we are editing other peoples accounts.
5342 if ('\0' == getStrH(Rd->queries, "user")[0])
5343 {
5344// Rd->shs.level = atoi(getStrH(Rd->database, "UserAccounts.UserLevel"));
5345// TODO - might have to combine first and last here.
5346// Rd->shs.name = Rd->database->getstr(Rd->database, "Lua.name", true);
5347// Rd->shs.UUID = Rd->database->getstr(Rd->database, "UserAccounts.PrincipalID", true);
5348//d("accountRead() setting session uuid %s level %d name %s ", Rd->shs.UUID, (int) Rd->shs.level, Rd->shs.name);
5349 }
5350// Rd->stuff->putstr(Rd->stuff, "email", getStrH(Rd->database, "UserAccounts.Email"));
5351 }
5352
5353 free(last);
5354 free(first);
5355 tnm->free(tnm);
5356 return ret;
5357}
5358
5359static int accountDelSub(reqData *Rd, inputForm *iF, inputValue *iV)
5360{
5361 int ret = 0;
5362 char *uuid = Rd->shs.UUID, *first = getStrH(Rd->stuff, "firstName"), *last = getStrH(Rd->stuff, "lastName");
5363 int c = accountRead(Rd, uuid, first, last);
5364
5365 if (1 != c)
5366 {
5367 bitch(Rd, "Cannot delete account.", "Account doesn't exist.");
5368 ret++;
5369 }
5370 else
5371 {
5372// check if logged in user is allowed to delete this account
5373// delete user record
5374// log the user out if they are logged in
5375 }
5376 return ret;
5377}
5378
5379// The [create member] button on accountLoginWeb()
5380static int accountCreateSub(reqData *Rd, inputForm *iF, inputValue *iV)
5381{
5382 int ret = 0;
5383 char *uuid = Rd->shs.UUID, *first = getStrH(Rd->stuff, "name"), *last = NULL;
5384 int c = accountRead(Rd, uuid, first, last);
5385
5386 if (strcmp("POST", Rd->Method) == 0)
5387 {
5388 if (0 != c)
5389 {
5390 bitch(Rd, "Cannot create account.", "Account exists.");
5391 Rd->shs.status = SHS_NUKE;
5392 ret++;
5393 }
5394 else
5395 {
5396 char *salt = newSLOSsalt(Rd);
5397 char *h = checkSLOSpassword(Rd, salt, getStrH(Rd->body, "password"), NULL, NULL);
5398
5399 if (NULL == h)
5400 ret++;
5401 else
5402 {
5403 Rd->stuff->putstr(Rd->stuff, "passHash", h);
5404 Rd->stuff->putstr(Rd->stuff, "passSalt", salt);
5405 if (NULL != Rd->shs.name) free(Rd->shs.name);
5406 // So that we can get the name later when we show the account data entry page via GET.
5407 Rd->shs.name = Rd->stuff->getstr(Rd->stuff, "name", true);
5408 free(h);
5409 Rd->shs.status = SHS_REFRESH;
5410 }
5411 free(salt);
5412 if (0 != ret)
5413 Rd->shs.status = SHS_NUKE;
5414 }
5415 }
5416 return ret;
5417}
5418
5419// The [confirm] button on accountAddWeb()
5420static int accountAddSub(reqData *Rd, inputForm *iF, inputValue *iV)
5421{
5422 int ret = 0;
5423 char *uuid = Rd->shs.UUID, *first = getStrH(Rd->stuff, "firstName"), *last = getStrH(Rd->stuff, "lastName");
5424 int c = accountRead(Rd, uuid, first, last);
5425
5426 if (0 != c)
5427 {
5428 bitch(Rd, "Cannot add account.", "Account exists.");
5429 Rd->shs.status = SHS_NUKE;
5430 ret++;
5431 }
5432 else if ((0 == ret) && (strcmp("POST", Rd->Method) == 0))
5433 {
5434 char *h = checkSLOSpassword(Rd, getStrH(Rd->stuff, "passSalt"), getStrH(Rd->stuff, "password"), getStrH(Rd->stuff, "passHash"), "Passwords are not the same.");
5435
5436 if (NULL == h)
5437 {
5438 ret++;
5439 Rd->shs.status = SHS_NUKE;
5440 }
5441 else
5442 {
5443 free(h);
5444 generateAccountUUID(Rd);
5445 Rd->stuff->putstr(Rd->stuff, "passwordHash", getStrH(Rd->stuff, "passHash"));
5446 Rd->stuff->putstr(Rd->stuff, "passwordSalt", getStrH(Rd->stuff, "passSalt"));
5447 Rd->shs.level = -200;
5448 Rd->database->putstr(Rd->database, "UserAccounts.UserLevel", "-200");
5449 // Generate the linky for the email.
5450 newSesh(Rd, TRUE);
5451 accountWrite(Rd);
5452 // log them in
5453 I("Logged on %s %s Level %d %s", Rd->shs.UUID, Rd->shs.name, Rd->shs.level, getLevel(Rd->shs.level));
5454 Rd->output = "accountView";
5455 Rd->form = "accountView";
5456 Rd->doit = "login";
5457 Rd->shs.status = SHS_LOGIN;
5458 }
5459 }
5460 return ret;
5461}
5462
5463static int accountSaveSub(reqData *Rd, inputForm *iF, inputValue *iV)
5464{
5465 int ret = 0;
5466 // Using body[user] here, coz we got to this page via a URL query.
5467 char *uuid = Rd->shs.UUID, *first = getStrH(Rd->body, "user"), *last = NULL;
5468 int c = accountRead(Rd, NULL, first, last);
5469
5470 if (1 != c)
5471 {
5472 bitch(Rd, "Cannot save account.", "Account doesn't exist.");
5473 ret++;
5474 }
5475 else if ((0 == ret) && (strcmp("POST", Rd->Method) == 0))
5476 {
5477 Rd->stuff->putstr(Rd->stuff, "email", getStrH(Rd->database, "UserAccounts.Email"));
5478 Rd->stuff->putstr(Rd->stuff, "created", getStrH(Rd->database, "UserAccounts.Created"));
5479 Rd->stuff->putstr(Rd->stuff, "flags", getStrH(Rd->database, "UserAccounts.UserFlags"));
5480 Rd->stuff->putstr(Rd->stuff, "active", getStrH(Rd->database, "UserAccounts.active"));
5481 Rd->stuff->putstr(Rd->stuff, "passwordSalt", getStrH(Rd->database, "auth.passwordSalt"));
5482 Rd->stuff->putstr(Rd->stuff, "passwordHash", getStrH(Rd->database, "auth.passwordHash"));
5483 Rd->stuff->putstr(Rd->stuff, "name", getStrH(Rd->database, "Lua.name"));
5484 Rd->stuff->putstr(Rd->stuff, "DoB", getStrH(Rd->database, "Lua.DoB"));
5485 Rd->stuff->putstr(Rd->stuff, "agree", getStrH(Rd->database, "Lua.agree"));
5486 Rd->stuff->putstr(Rd->stuff, "adult", getStrH(Rd->database, "Lua.adult"));
5487 Rd->stuff->putstr(Rd->stuff, "aboutMe", getStrH(Rd->database, "Lua.aboutMe"));
5488 Rd->stuff->putstr(Rd->stuff, "vouched", getStrH(Rd->database, "Lua.vouched"));
5489 Rd->stuff->putstr(Rd->stuff, "voucher", getStrH(Rd->database, "Lua.voucher"));
5490
5491 char *lvl = getStrH(Rd->body, "level");
5492 qlisttbl_obj_t obj;
5493
5494 memset((void*)&obj, 0, sizeof(obj)); // must be cleared before call
5495 accountLevels->lock(accountLevels);
5496 while(accountLevels->getnext(accountLevels, &obj, NULL, false) == true)
5497 {
5498 if (strcmp(lvl, (char *) obj.data) == 0)
5499 Rd->database->putstr(Rd->database, "UserAccounts.UserLevel", obj.name);
5500 }
5501 accountLevels->unlock(accountLevels);
5502 accountWrite(Rd);
5503 free(Rd->outQuery);
5504 Rd->outQuery = xmprintf("?user=%s+%s", getStrH(Rd->database, "UserAccounts.FirstName"), getStrH(Rd->database, "UserAccounts.LastName"));
5505// TODO - this isn't being shown.
5506 addStrL(Rd->messages, "Account saved.");
5507 }
5508 return ret;
5509}
5510
5511// The unique validation URL sent in email.
5512static int accountValidateSub(reqData *Rd, inputForm *iF, inputValue *iV)
5513{
5514 int ret = 0;
5515 char *uuid = Rd->shs.UUID, *first = getStrH(Rd->stuff, "firstName"), *last = getStrH(Rd->stuff, "lastName");
5516 int c = accountRead(Rd, uuid, first, last);
5517
5518 if (1 != c)
5519 {
5520 bitch(Rd, "Cannot validate account.", "Account doesn't exist.");
5521 ret++;
5522 }
5523 else
5524 {
5525 Rd->stuff->putstr(Rd->stuff, "email", getStrH(Rd->database, "UserAccounts.Email"));
5526 Rd->stuff->putstr(Rd->stuff, "created", getStrH(Rd->database, "UserAccounts.Created"));
5527 Rd->stuff->putstr(Rd->stuff, "flags", getStrH(Rd->database, "UserAccounts.UserFlags"));
5528 Rd->stuff->putstr(Rd->stuff, "active", getStrH(Rd->database, "UserAccounts.active"));
5529 Rd->stuff->putstr(Rd->stuff, "passwordSalt", getStrH(Rd->database, "auth.passwordSalt"));
5530 Rd->stuff->putstr(Rd->stuff, "passwordHash", getStrH(Rd->database, "auth.passwordHash"));
5531 Rd->stuff->putstr(Rd->stuff, "name", getStrH(Rd->database, "Lua.name"));
5532 Rd->stuff->putstr(Rd->stuff, "DoB", getStrH(Rd->database, "Lua.DoB"));
5533 Rd->stuff->putstr(Rd->stuff, "agree", getStrH(Rd->database, "Lua.agree"));
5534 Rd->stuff->putstr(Rd->stuff, "adult", getStrH(Rd->database, "Lua.adult"));
5535 Rd->stuff->putstr(Rd->stuff, "aboutMe", getStrH(Rd->database, "Lua.aboutMe"));
5536 Rd->stuff->putstr(Rd->stuff, "vouched", getStrH(Rd->database, "Lua.vouched"));
5537 Rd->stuff->putstr(Rd->stuff, "voucher", getStrH(Rd->database, "Lua.voucher"));
5538 Rd->shs.level = -100;
5539 Rd->database->putstr(Rd->database, "UserAccounts.UserLevel", "-100");
5540 accountWrite(Rd);
5541 Rd->doit = "logout";
5542 Rd->output = "accountLogin";
5543 Rd->form = "accountLogin";
5544 Rd->shs.status = SHS_NUKE;
5545 }
5546 return ret;
5547}
5548
5549static int accountViewSub(reqData *Rd, inputForm *iF, inputValue *iV)
5550{
5551// TODO - this has to change when we are editing other peoples accounts.
5552 int ret = 0;
5553 char *uuid = Rd->shs.UUID, *first = getStrH(Rd->stuff, "firstName"), *last = getStrH(Rd->stuff, "lastName");
5554 int c = accountRead(Rd, uuid, first, last);
5555
5556d("Sub accountViewSub() %s %s %s", uuid, first, last);
5557 if (1 != c)
5558 {
5559 bitch(Rd, "Cannot view account.", "Account doesn't exist.");
5560 ret++;
5561 Rd->shs.status = SHS_NUKE;
5562 }
5563 else
5564 {
5565 // Check password on POST if the session user is the same as the shown user, coz this is the page shown on login.
5566 // Also only check on login.
5567 if ((strcmp("POST", Rd->Method) == 0) //&& (strcmp(Rd->shs.UUID, getStrH(Rd->database, "UserAccounts.PrincipalID")) == 0)
5568 && (strcmp("login", Rd->doit) == 0) && (strcmp("accountLogin", Rd->form) == 0))
5569 {
5570 char *h = checkSLOSpassword(Rd, getStrH(Rd->database, "auth.passwordSalt"), getStrH(Rd->body, "password"), getStrH(Rd->database, "auth.passwordHash"), "Login failed.");
5571 if (NULL == h)
5572 {
5573 ret++;
5574 Rd->shs.status = SHS_NUKE;
5575 }
5576 else
5577 {
5578 Rd->shs.level = atoi(getStrH(Rd->database, "UserAccounts.UserLevel"));
5579 if (NULL != Rd->shs.name) free(Rd->shs.name);
5580 Rd->shs.name = Rd->database->getstr(Rd->database, "Lua.name", true);
5581 if (NULL != Rd->shs.UUID) free(Rd->shs.UUID);
5582 Rd->shs.UUID = Rd->database->getstr(Rd->database, "UserAccounts.PrincipalID", true);
5583 free(h);
5584 I("Logged on %s %s Level %d %s", Rd->shs.UUID, Rd->shs.name, Rd->shs.level, getLevel(Rd->shs.level));
5585 Rd->shs.status = SHS_LOGIN;
5586 }
5587 }
5588 }
5589
5590 return ret;
5591}
5592static int accountEditSub(reqData *Rd, inputForm *iF, inputValue *iV)
5593{
5594 int ret = 0;
5595 char *uuid = Rd->shs.UUID, *first = getStrH(Rd->stuff, "firstName"), *last = getStrH(Rd->stuff, "lastName");
5596 int c = accountRead(Rd, uuid, first, last);
5597
5598d("Sub accountEditSub %s %s %s", uuid, first, last);
5599 if (1 != c)
5600 {
5601 bitch(Rd, "Cannot edit account.", "Account doesn't exist.");
5602 ret++;
5603 }
5604 else
5605 {
5606// check if logged in user is allowed to make these changes
5607// update user record
5608 }
5609 return ret;
5610}
5611
5612static int accountExploreSub(reqData *Rd, inputForm *iF, inputValue *iV)
5613{
5614 int ret = 0;
5615// get a list of user records
5616 return ret;
5617}
5618
5619static int accountOutSub(reqData *Rd, inputForm *iF, inputValue *iV)
5620{
5621 int ret = 0;
5622 char *uuid = Rd->shs.UUID, *first = getStrH(Rd->stuff, "firstName"), *last = getStrH(Rd->stuff, "lastName");
5623 int c = accountRead(Rd, uuid, first, last);
5624
5625 if (1 != c)
5626 {
5627// bitch(Rd, "Cannot logout account.", "Account doesn't exist.");
5628// ret++;
5629 }
5630
5631 Rd->shs.status = SHS_NUKE;
5632 return ret;
5633}
5634
5635/* TODO - instead of searching through all the users, ...
5636 have a bunch of separate folders with symlinks
5637 scData/users/aaproved
5638 scData/users/disabled
5639 scData/users/god
5640 onefang_rejected.lua -> ../uuid.lua
5641 scData/users/newbie
5642 foo_bar.lua -> ../uuid.lua
5643 scData/users/validated
5644
5645*/
5646typedef struct _RdAndListTbl RdAndListTbl;
5647struct _RdAndListTbl
5648{
5649 reqData *Rd;
5650 qlisttbl_t *list;
5651};
5652static int accountFilterValidated(struct dirtree *node)
5653{
5654 if (!node->parent) return DIRTREE_RECURSE | DIRTREE_SHUTUP;
5655
5656 if (S_ISREG(node->st.st_mode))
5657 {
5658 struct stat st;
5659 struct timespec now;
5660 RdAndListTbl *rdl = (RdAndListTbl *) node->parent->extra;
5661 qhashtbl_t *tnm = qhashtbl(0, 0);
5662 char *name = node->name;
5663 char *where = xmprintf("%s/users/%s", scData, node->name);
5664 int rt = LuaToHash(rdl->Rd, where, "user", tnm, 0, &st, &now, "user");
5665
5666t("accountFilterValidatedVoucher %s (%s) -> %s -> %s", name, getStrH(tnm, "level"), getStrH(tnm, "name"), getStrH(tnm, "voucher"));
5667 if ((0 == rt) && (strcmp("-100", getStrH(tnm, "level")) == 0))
5668 rdl->list->put(rdl->list, getStrH(tnm, "name"), &tnm, sizeof(qhashtbl_t *));
5669 else
5670 tnm->free(tnm);
5671 free(where);
5672 }
5673 return 0;
5674}
5675qlisttbl_t *getAccounts(reqData *Rd)
5676{
5677 qlisttbl_t *ret = qlisttbl(0);
5678 RdAndListTbl rdl = {Rd, ret};
5679 char *path = xmprintf("%s/users", scData);
5680 struct dirtree *new = dirtree_add_node(0, path, 0);
5681
5682 new->extra = (long) &rdl;
5683 dirtree_handle_callback(new, accountFilterValidated);
5684 ret->sort(ret);
5685 free(path);
5686
5687 return ret;
5688}
5689static void accountExploreValidatedVouchersWeb(reqData *Rd, inputForm *oF, inputValue *oV)
5690{
5691 qlisttbl_t *list =getAccounts(Rd);
5692
5693 if (NULL != Rd->shs.name) free(Rd->shs.name);
5694 Rd->shs.name = NULL;
5695 if (NULL != Rd->shs.UUID) free(Rd->shs.UUID);
5696 Rd->shs.UUID = NULL;
5697 Rd->shs.level = -256;
5698 accountWebHeaders(Rd, oF);
5699 accountWebFields(Rd, oF, oV);
5700
5701 count = list->size(list);
5702 Rd->reply->addstrf(Rd->reply, "<table border=\"1\"><caption>Validated users</caption>\n");
5703 Rd->reply->addstr(Rd->reply, "<tr>");
5704 Rd->reply->addstr(Rd->reply, "<th>name</th>");
5705 Rd->reply->addstr(Rd->reply, "<th>voucher</th>");
5706 Rd->reply->addstr(Rd->reply, "<th>level</th>");
5707 Rd->reply->addstr(Rd->reply, "<th>title</th>");
5708 Rd->reply->addstr(Rd->reply, "</tr>\n<tr>");
5709
5710 qlisttbl_obj_t obj;
5711 memset((void *) &obj, 0, sizeof(obj));
5712 list->lock(list);
5713 while(list->getnext(list, &obj, NULL, false) == true)
5714 {
5715 qhashtbl_t *tnm = *((qhashtbl_t **) obj.data);
5716 char *nm = qstrreplace("tr", xstrdup(obj.name), " ", "+");
5717
5718 Rd->reply->addstrf(Rd->reply, "<tr><td><a href='https://%s%s%s?user=%s'>%s</a></td>", Rd->Host, Rd->Script, Rd->Path, nm, obj.name);
5719 Rd->reply->addstrf(Rd->reply, "<td>%s</td><td>%s</td><td>%s</td></tr>", getStrH(tnm, "voucher"), getStrH(tnm, "level"), getStrH(tnm, "title"));
5720 free(nm);
5721 tnm->clear(tnm);
5722 list->removeobj(list, &obj);
5723 tnm->free(tnm);
5724 }
5725 list->unlock(list);
5726 Rd->reply->addstr(Rd->reply, "</table>");
5727 list->free(list);
5728
5729 accountWebSubs(Rd, oF);
5730 accountWebFooter(Rd, oF);
5731}
5732static int accountExploreValidatedVoucherSub(reqData *Rd, inputForm *iF, inputValue *iV)
5733{
5734 int ret = 0;
5735 return ret;
5736}
5737
5738
5739qhashtbl_t *accountPages = NULL;
5740inputForm *newInputForm(char *name, char *title, char *help, inputFormShowFunc web, inputFormShowFunc eWeb)
5741{
5742 inputForm *ret = xmalloc(sizeof(inputForm));
5743
5744d("newInputForm(%s)", name);
5745 ret->name = name; ret->title = title; ret->help = help;
5746 ret->web = web; ret->eWeb = eWeb;
5747 ret->fields = qlisttbl(QLISTTBL_THREADSAFE | QLISTTBL_UNIQUE | QLISTTBL_LOOKUPFORWARD);
5748 ret->subs = qhashtbl(0, 0);
5749 accountPages->put(accountPages, ret->name, ret, sizeof(inputForm));
5750 free(ret);
5751 return accountPages->get(accountPages, name, NULL, false);
5752}
5753
5754inputField *addInputField(inputForm *iF, signed char type, char *name, char *title, char *help, inputFieldValidFunc validate, inputFieldShowFunc web)
5755{
5756 inputField *ret = xzalloc(sizeof(inputField));
5757
5758//d("addInputField(%s, %s)", iF->name, name);
5759 ret->name = name; ret->title = title; ret->help = help;
5760 ret->validate = validate; ret->web = web; ret->type = type;
5761 ret->flags = FLD_EDITABLE;
5762 iF->fields->put(iF->fields, ret->name, ret, sizeof(inputField));
5763 free(ret);
5764 return iF->fields->get(iF->fields, name, NULL, false);
5765}
5766
5767void inputFieldExtra(inputField *ret, signed char flags, short viewLength, short maxLength)
5768{
5769 ret->flags = flags;
5770 ret->viewLength = viewLength; ret->maxLength = maxLength;
5771}
5772
5773void addSession(inputForm *iF)
5774{
5775 inputField *fld, **flds = xzalloc(3 * sizeof(*flds));
5776
5777//d("addSession(%s)", iF->name);
5778 flds[0] = addInputField(iF, LUA_TSTRING, "hashish", "hashish", "", sessionValidate, sessionWeb);
5779 inputFieldExtra(flds[0], FLD_HIDDEN, 0, 0);
5780 flds[1] = addInputField(iF, LUA_TSTRING, "toke_n_munchie", "toke_n_munchie", "", sessionValidate, sessionWeb);
5781 inputFieldExtra(flds[1], FLD_HIDDEN, 0, 0);
5782 fld = addInputField(iF, LUA_TGROUP, "sessionGroup", "sessionGroup", "", sessionValidate, sessionWeb);
5783 inputFieldExtra(fld, FLD_HIDDEN, 0, 0);
5784 fld->group = flds;
5785 flds[0]->group = flds;
5786 flds[1]->group = flds;
5787}
5788
5789void addEmailFields(inputForm *iF)
5790{
5791 inputField *fld, **flds = xzalloc(3 * sizeof(*flds));
5792
5793 flds[0] = addInputField(iF, LUA_TEMAIL, "email", "email", NULL, emailValidate, emailWeb);
5794 inputFieldExtra(flds[0], FLD_EDITABLE, 42, 254);
5795 flds[1] = addInputField(iF, LUA_TEMAIL, "emayl", "Re-enter your email, to be sure you got it correct",
5796 "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);
5797 inputFieldExtra(flds[1], FLD_EDITABLE, 42, 254);
5798 fld = addInputField(iF, LUA_TGROUP, "emailGroup", "emailGroup", "", emailValidate, NULL);
5799 inputFieldExtra(fld, FLD_HIDDEN, 0, 0);
5800 fld->group = flds;
5801 flds[0]->group = flds;
5802 flds[1]->group = flds;
5803}
5804
5805void addDoBFields(inputForm *iF)
5806{
5807 inputField *fld, **flds = xzalloc(3 * sizeof(*flds));
5808
5809 flds[0] = addInputField(iF, LUA_TSTRING, "DoByear", "year", NULL, DoBValidate, DoByWeb);
5810 flds[1] = addInputField(iF, LUA_TSTRING, "DoBmonth", "month", NULL, DoBValidate, DoBmWeb);
5811 fld = addInputField(iF, LUA_TGROUP, "DoBGroup", "DoBGroup", "", DoBValidate, DoBWeb);
5812 inputFieldExtra(fld, FLD_HIDDEN, 0, 0);
5813 fld->group = flds;
5814 flds[0]->group = flds;
5815 flds[1]->group = flds;
5816}
5817
5818void addLegalFields(inputForm *iF)
5819{
5820 inputField *fld, **flds = xzalloc(3 * sizeof(*flds));
5821
5822 flds[0] = addInputField(iF, LUA_TBOOLEAN, "adult", "I'm allegedly an adult in my country.", NULL, legalValidate, adultWeb);
5823 flds[1] = addInputField(iF, LUA_TBOOLEAN, "agree", "I accept the Terms of Service.", NULL, legalValidate, agreeWeb);
5824 fld = addInputField(iF, LUA_TGROUP, "legalGroup", "legalGroup", "", legalValidate, legalWeb);
5825 inputFieldExtra(fld, FLD_HIDDEN, 0, 0);
5826 fld->group = flds;
5827 flds[0]->group = flds;
5828 flds[1]->group = flds;
5829}
5830
5831inputSub *addSubmit(inputForm *iF, char *name, char *title, char *help, inputSubmitFunc submit, char *output)
5832{
5833 inputSub *ret = xmalloc(sizeof(inputSub));
5834
5835//d("addSubmit(%s, %s)", iF->name, name);
5836 ret->name = name; ret->title = title; ret->help = help; ret->submit = submit; ret->outputForm = output;
5837 iF->subs->put(iF->subs, ret->name, ret, sizeof(inputSub));
5838 free(ret);
5839 return iF->subs->get(iF->subs, name, NULL, false);
5840}
5841
5842
5843/* There should be some precedence for values overriding values here.
5844 https://www.w3.org/standards/webarch/protocols
5845 "This intro text is boilerplate for the beta release of w3.org." Fucking useless. Pffft
5846 https://www.w3.org/Protocols/
5847 Still nothing official, though the ENV / HEADER stuff tends to be about the protocol things, and cookies / body / queries are about the data things.
5848 http://docs.gantry.org/gantry4/advanced/setby
5849 Says that query overrides cookies, but that might be just for their platform.
5850 https://framework.zend.com/manual/1.11/en/zend.controller.request.html
5851 Says - "1. GET, 2. POST, 3. COOKIE, 4. SERVER, 5. ENV."
5852 We don't actually get the headers directly, it's all sent via the env.
5853
5854URL query Values actually provided by the user in the FORM, and other things.
5855POST body Values actually provided by the user in the FORM.
5856cookies
5857 https://stackoverflow.com/questions/4056306/how-to-handle-multiple-cookies-with-the-same-name
5858
5859headers includes HTTP_COOKIE and QUERY_STRING
5860env includes headers and HTTP_COOKIE and QUERY_STRING
5861
5862database Since all of the above are for updating the database anyway, this goes on the bottom, overridden by all.
5863 Though be wary of security stuff.
5864
5865 Sending cookie headers is a special case, multiples can be sent, otherwise headers are singletons, only send one for each name.
5866*/
5867char *sourceTypes[] =
5868{
5869 "cookies",
5870 "body",
5871 "queries",
5872 "stuff"
5873};
5874
5875static int collectFields(reqData *Rd, inputForm *iF, inputValue *iV, int t)
5876{
5877 int i = 0, j;
5878 qlisttbl_obj_t obj;
5879
5880 memset((void*)&obj, 0, sizeof(obj)); // must be cleared before call
5881 iF->fields->lock(iF->fields);
5882 while(iF->fields->getnext(iF->fields, &obj, NULL, false) == true)
5883 {
5884 inputField *fld = (inputField *) obj.data;
5885
5886//if (0 > t)
5887// d("Collecting %d %s - %s", t, iF->name, fld->name);
5888//else
5889// d("Collecting %s %s - %s", sourceTypes[t], iF->name, fld->name);
5890 iV[i].field = fld;
5891 if (LUA_TGROUP == fld->type)
5892 {
5893 if (0 >= t)
5894 {
5895 j = 0;
5896 // If it's a group, number the members relative to the group field.
5897 // Assume the members for this group are the previous ones.
5898 while (iV[i].field->group[j])
5899 {
5900 j++;
5901 iV[i - j].index = j;
5902 }
5903 }
5904 }
5905 else
5906 {
5907 char *vl = NULL;
5908
5909 switch (t)
5910 {
5911 // We don't get the cookies metadata.
5912 case 0 : vl = Rd->cookies->getstr(Rd->cookies, obj.name, false); break;
5913 case 1 : vl = Rd->body-> getstr(Rd->body, obj.name, false); break;
5914 case 2 : vl = Rd->queries->getstr(Rd->queries, obj.name, false); break;
5915 case 3 : vl = Rd->queries->getstr(Rd->stuff, obj.name, false); break;
5916 default: break;
5917 }
5918 if ((NULL != iV[i].value) && (NULL != vl))
5919 {
5920 if (strcmp(vl, iV[i].value) != 0)
5921 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]);
5922 else
5923 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]);
5924 }
5925 if (NULL != vl)
5926 {
5927 iV[i].source = t;
5928 iV[i].value = vl;
5929 D("Collected %s value for %s - %s = %s", sourceTypes[t], iF->name, fld->name, vl);
5930 }
5931 }
5932 i++;
5933 }
5934 iF->fields->unlock(iF->fields);
5935 return i;
5936}
5937
5938
5939void sessionStateEngine(reqData *Rd, char *type)
5940{
5941 switch (Rd->shs.status)
5942 {
5943 case SHS_UNKNOWN: d("sessionStateEngine(SHS_UNKNOWN, %s)", type); break;
5944 case SHS_NONE: d("sessionStateEngine(SHS_NONE, %s)", type); break;
5945 case SHS_BOGUS: d("sessionStateEngine(SHS_BOGUS, %s)", type); break;
5946 case SHS_PROBLEM: d("sessionStateEngine(SHS_PROBLEM, %s)", type); break;
5947 case SHS_VALID: d("sessionStateEngine(SHS_VALID, %s)", type); break;
5948
5949 case SHS_LOGIN: d("sessionStateEngine(SHS_LOGIN, %s)", type); break;
5950
5951 case SHS_RENEW: d("sessionStateEngine(SHS_RENEW, %s)", type); break;
5952 case SHS_REFRESH: d("sessionStateEngine(SHS_REFRESH, %s)", type); break;
5953 case SHS_IDLE: d("sessionStateEngine(SHS_IDLE, %s)", type); break;
5954 case SHS_ANCIENT: d("sessionStateEngine(SHS_ANCIENT, %s)", type); break;
5955
5956 case SHS_SECURITY: d("sessionStateEngine(SHS_SECURITY, %s)", type); break;
5957 case SHS_RELOGIN: d("sessionStateEngine(SHS_RELOGIN, %s)", type); break;
5958
5959 case SHS_KEEP: d("sessionStateEngine(SHS_KEEP, %s)", type); break;
5960 case SHS_WIPE: d("sessionStateEngine(SHS_WIPE, %s)", type); break;
5961 case SHS_NUKE: d("sessionStateEngine(SHS_NUKE, %s)", type); break;
5962 }
5963}
5964
5965
5966void account_html(char *file, reqData *Rd, HTMLfile *thisFile)
5967{
5968 inputForm *iF;
5969 inputField *fld;
5970 inputSub *sub;
5971 boolean isGET = FALSE;
5972 int e = 0, t = 0, i, j;
5973 char *doit = getStrH(Rd->body, "doit"), *form = getStrH(Rd->body, "form");
5974
5975 if (NULL == accountLevels)
5976 {
5977 accountLevels = qlisttbl(QLISTTBL_LOOKUPFORWARD | QLISTTBL_THREADSAFE | QLISTTBL_UNIQUE);
5978 accountLevels->putstr(accountLevels, "-256", "disabled");
5979 accountLevels->putstr(accountLevels, "-200", "newbie");
5980 accountLevels->putstr(accountLevels, "-100", "validated");
5981 accountLevels->putstr(accountLevels, "-50", "vouched for");
5982 accountLevels->putstr(accountLevels, "0", "approved"); // Note that http://opensimulator.org/wiki/Userlevel claims that 1 and above are "GOD_LIKE".
5983 accountLevels->putstr(accountLevels, "200", "god");
5984 }
5985
5986 // Check "Origin" header and /or HTTP_REFERER header.
5987 // "Origin" is either HTTP_HOST or X-FORWARDED-HOST. Which could be "null".
5988 char *ref = xmprintf("https://%s%s/account.html", getStrH(Rd->headers, "SERVER_NAME"), getStrH(Rd->headers, "SCRIPT_NAME"));
5989 char *href = Rd->headers->getstr(Rd->headers, "HTTP_REFERER", true);
5990
5991 if (NULL != href)
5992 {
5993 char *f = strchr(href, '?');
5994
5995 if (NULL != f)
5996 *f = '\0';
5997 if (('\0' != href[0]) && (strcmp(ref, href) != 0))
5998 {
5999 bitch(Rd, "Invalid referer.", ref);
6000 D("Invalid referer - %s isn't %s", ref, href);
6001 form = "accountLogin";
6002 Rd->shs.status = SHS_PROBLEM;
6003 }
6004 free(href);
6005 }
6006 free(ref);
6007 ref = getStrH(Rd->headers, "SERVER_NAME");
6008 href = getStrH(Rd->headers, "HTTP_HOST");
6009 if ('\0' == href[0])
6010 href = getStrH(Rd->headers, "X-FORWARDED-HOST");
6011 if (('\0' != href[0]) && (strcmp(ref, href) != 0))
6012 {
6013 bitch(Rd, "Invalid HOST.", ref);
6014 D("Invalid HOST - %s isn't %s", ref, href);
6015 form = "accountLogin";
6016 Rd->shs.status = SHS_PROBLEM;
6017 }
6018
6019 // Redirect to HTTPS if it's HTTP.
6020 if (strcmp("https", Rd->Scheme) != 0)
6021 {
6022 Rd->Rheaders->putstr (Rd->Rheaders, "Status", "301 Moved Permanently");
6023 Rd->Rheaders->putstrf(Rd->Rheaders, "Location", "https://%s%s", Rd->Host, Rd->RUri);
6024 Rd->reply->addstrf(Rd->reply, "<html><title>404 Unknown page</title><head>"
6025 "<meta http-equiv='refresh' content='0; URL=https://%s%s' />"
6026 "</head><body>You should get redirected to <a href='https://%s%s'>https://%s%s</a></body></html>",
6027 Rd->Host, Rd->RUri, Rd->Host, Rd->RUri, Rd->Host, Rd->RUri
6028 );
6029 D("Redirecting dynamic page %s -> https://%s%s", file, Rd->Host, Rd->RUri);
6030 return;
6031 }
6032
6033 // Create the dynamic web pages for account.html.
6034 if (NULL == accountPages)
6035 {
6036 accountPages = qhashtbl(0, 0);
6037
6038
6039 iF = newInputForm("accountAdd", "", NULL, accountAddWeb, accountLoginWeb);
6040 addSession(iF);
6041// fld = addInputField(iF, LUA_TSTRING, "UUID", "UUID", NULL, UUIDValidate, UUIDWeb);
6042// inputFieldExtra(fld, FLD_HIDDEN, 0, 0);
6043 fld = addInputField(iF, LUA_TSTRING, "name", "name", NULL, nameValidate, nameWeb);
6044 inputFieldExtra(fld, FLD_EDITABLE | FLD_REQUIRED, 42, 63);
6045 fld = addInputField(iF, LUA_TPASSWORD, "psswrd", "Re-enter your password",
6046 "Warning, the limit on password length is set by your viewer, some can't handle longer than 16 characters.", passwordValidate, passwordWeb);
6047 inputFieldExtra(fld, FLD_EDITABLE, 16, 0);
6048 addEmailFields(iF);
6049 addDoBFields(iF);
6050 addLegalFields(iF);
6051 fld = addInputField(iF, LUA_TSTRING, "ToS", "Terms of Service", "", NULL, ToSWeb);
6052 fld = addInputField(iF, LUA_TSTRING, "voucher", "The grid name of someone that will vouch for you",
6053 "We use a vouching system here, an existing member must know you well enough to tell us you'll be good for our grid.", voucherValidate, voucherWeb);
6054 inputFieldExtra(fld, FLD_EDITABLE, 42, 63);
6055 fld = addInputField(iF, LUA_TSTRING, "aboutMe", "About me", NULL, aboutMeValidate, aboutMeWeb);
6056 inputFieldExtra(fld, FLD_EDITABLE, 50, 16384);
6057 addSubmit(iF, "confirm", "confirm", NULL, accountAddSub, "accountView");
6058 addSubmit(iF, "cancel", "cancel", NULL, accountOutSub, "accountLogin");
6059
6060
6061 iF = newInputForm("accountView", "account view", NULL, accountViewWeb, accountLoginWeb);
6062 addSession(iF);
6063// fld = addInputField(iF, LUA_TSTRING, "UUID", "UUID", NULL, UUIDValidate, UUIDWeb);
6064// inputFieldExtra(fld, FLD_HIDDEN, 0, 0);
6065 fld = addInputField(iF, LUA_TSTRING, "name", "name", NULL, nameValidate, nameWeb);
6066 inputFieldExtra(fld, FLD_HIDDEN, 42, 63);
6067 fld = addInputField(iF, LUA_TSTRING, "user", "user", NULL, nameValidate, nameWeb);
6068 inputFieldExtra(fld, FLD_HIDDEN, 42, 63);
6069 addSubmit(iF, "login", "", NULL, accountViewSub, "accountView"); // Coz we sometimes want to trigger this from code.
6070 addSubmit(iF, "validate", "", NULL, accountValidateSub, "accountLogin"); // Coz we sometimes want to trigger this from code.
6071 addSubmit(iF, "edit", "", NULL, accountEditSub, "accountEdit"); // Coz we sometimes want to trigger this from code.
6072 addSubmit(iF, "validated_members", "validated members", NULL, accountExploreValidatedVoucherSub, "accountValidated");
6073 addSubmit(iF, "logout", "logout", NULL, accountOutSub, "accountLogin");
6074
6075
6076 iF = newInputForm("accountValidated", "account validated list", NULL, accountExploreValidatedVouchersWeb, accountLoginWeb);
6077 addSession(iF);
6078 fld = addInputField(iF, LUA_TSTRING, "name", "name", NULL, nameValidate, nameWeb);
6079 inputFieldExtra(fld, FLD_HIDDEN, 42, 63);
6080 addSubmit(iF, "login", "", NULL, accountViewSub, "accountView"); // Coz we sometimes want to trigger this from code.
6081 addSubmit(iF, "back", "back", NULL, accountViewSub, "accountView");
6082
6083
6084 iF = newInputForm("accountEdit", "account edit", NULL, accountEditWeb, accountLoginWeb);
6085 addSession(iF);
6086// fld = addInputField(iF, LUA_TSTRING, "UUID", "UUID", NULL, UUIDValidate, UUIDWeb);
6087// inputFieldExtra(fld, FLD_HIDDEN, 0, 0);
6088// fld = addInputField(iF, LUA_TSTRING, "name", "name", NULL, nameValidate, nameWeb);
6089// inputFieldExtra(fld, FLD_HIDDEN, 42, 63);
6090// fld = addInputField(iF, LUA_TSTRING, "user", "user", NULL, nameValidate, nameWeb);
6091// inputFieldExtra(fld, FLD_HIDDEN, 42, 63);
6092// fld = addInputField(iF, LUA_TEMAIL, "email", "email", "", emailValidate, emailWeb);
6093// inputFieldExtra(fld, FLD_NONE, 42, 254);
6094 addSubmit(iF, "login", "", NULL, accountViewSub, "accountView"); // Coz we sometimes want to trigger this from code.
6095 addSubmit(iF, "save", "save", NULL, accountSaveSub, "accountView");
6096 addSubmit(iF, "back", "back", NULL, accountViewSub, "accountView");
6097// addSubmit(iF, "members", "members", NULL, accountExploreSub, "accountExplore");
6098 addSubmit(iF, "logout", "logout", NULL, accountOutSub, "accountLogin");
6099// addSubmit(iF, "delete", "delete", NULL, accountDelSub, "accountDel");
6100
6101
6102 iF = newInputForm("accountLogin", "account login", "Please login, or create your new account.", accountLoginWeb, accountLoginWeb);
6103 addSession(iF);
6104 fld = addInputField(iF, LUA_TSTRING, "name", "name", "Your name needs to be two words, only ordinary letters and digits, no special characters or fonts.", nameValidate, nameWeb);
6105 inputFieldExtra(fld, FLD_EDITABLE | FLD_REQUIRED, 42, 63);
6106 fld = addInputField(iF, LUA_TPASSWORD, "password", "password",
6107 "Warning, the limit on password length is set by your viewer, some can't handle longer than 16 characters.", passwordValidate, passwordWeb);
6108 inputFieldExtra(fld, FLD_EDITABLE | FLD_REQUIRED, 16, 0);
6109 addSubmit(iF, "logout", "", NULL, accountOutSub, "accountLogin"); // Coz we sometimes want to trigger this from code.
6110 addSubmit(iF, "validate", "", NULL, accountValidateSub, "accountLogin"); // Coz we sometimes want to trigger this from code.
6111 addSubmit(iF, "login", "login", NULL, accountViewSub, "accountView");
6112 addSubmit(iF, "create", "create account", NULL, accountCreateSub, "accountAdd");
6113 }
6114
6115 // Figure out what we are doing.
6116 if ('\0' == form[0])
6117 form = getStrH(Rd->cookies, "form");
6118 if ('\0' == form[0])
6119 {
6120 form = "accountLogin";
6121 Rd->shs.status = SHS_NUKE;
6122 }
6123 if ('\0' == doit[0])
6124 doit = getStrH(Rd->cookies, "doit");
6125 if ('\0' == doit[0])
6126 {
6127 doit = "logout";
6128 Rd->shs.status = SHS_NUKE;
6129 }
6130 if ('\0' != doit[0])
6131 {
6132 setCookie(Rd, "doit", doit);
6133 Rd->doit = doit;
6134 }
6135
6136 iF = accountPages->get(accountPages, form, NULL, false);
6137 if (NULL == iF)
6138 {
6139 E("No such account page - %s", form);
6140 form = "accountLogin";
6141 doit = "logout";
6142 Rd->shs.status = SHS_PROBLEM;
6143 iF = accountPages->get(accountPages, form, NULL, false);
6144 }
6145 sub = iF->subs->get(iF->subs, doit, NULL, false);
6146 if (NULL == sub)
6147 {
6148 E("No such account action - %s", doit);
6149 form = "accountLogin";
6150 doit = "logout";
6151 Rd->shs.status = SHS_PROBLEM;
6152 iF = accountPages->get(accountPages, form, NULL, false);
6153 sub = iF->subs->get(iF->subs, doit, NULL, false);
6154 }
6155
6156 // Special for showing a users details.
6157 if ('\0' != getStrH(Rd->queries, "user")[0])
6158 {
6159 doit = "edit";
6160 form = "accountView";
6161 iF = accountPages->get(accountPages, form, NULL, false);
6162 sub = iF->subs->get(iF->subs, doit, NULL, false);
6163 }
6164
6165 Rd->doit = doit;
6166 Rd->form = form;
6167 Rd->output = sub->outputForm;
6168
6169 C("Starting dynamic page %s %s -> %s [%s -> %s]", Rd->RUri, form, doit, sub->name, Rd->output);
6170
6171 // Collect the input data.
6172 int count = iF->fields->size(iF->fields);
6173 inputValue *iV = xzalloc(count * sizeof(inputValue));
6174 qlisttbl_obj_t obj;
6175
6176 sessionStateEngine(Rd, "collected");
6177
6178 if (strcmp("cancel", sub->name) != 0)
6179 {
6180
6181// TODO - complain about any extra body or query parameter that we where not expecting.
6182 for (t = 0; t < 3; t++)
6183 i = collectFields(Rd, iF, iV, t);
6184
6185 // Validate the input data.
6186 D("For page %s -> %s, start of validation.", iF->name, sub->name);
6187 t = i;
6188 for (i = 0; i < t; i++)
6189 {
6190 if ((NULL != iV[i].value) || (LUA_TGROUP == iV[i].field->type))
6191 {
6192 if (NULL == iV[i].field->validate)
6193 E("No validation function for %s - %s", iF->name, iV[i].field->name);
6194 else
6195 {
6196 if (0 == iV[i].valid)
6197 {
6198 D("Validating %s - %s", iF->name, iV[i].field->name);
6199 int rt = iV[i].field->validate(Rd, iF, &iV[i]);
6200 if (rt)
6201 {
6202 W("Invalidated %s - %s returned %d errors", iF->name, iV[i].field->name, rt);
6203 iV[i].valid = -1;
6204 }
6205 else
6206 {
6207 D("Validated %s - %s", iF->name, iV[i].field->name);
6208 iV[i].valid = 1;
6209 }
6210 e += rt;
6211 if (NULL != iV[i].field->group)
6212 {
6213 // Use the indexes to set the validation result for the other members of the group.
6214 // The assumption is that the validation functions for each are the same, and it validates the members as a group.
6215 for (j = iV[i].index; j > 0; j--)
6216 iV[i + j].valid = iV[i].valid;
6217// TODO - Possible off by one error, but I don't think it matters. Needs more testing.
6218 for (j = 0; j <= iV[i].index; j++)
6219 iV[i + j].valid = iV[i].valid;
6220 }
6221 }
6222 else if (0 > iV[i].valid)
6223 D("Already invalidated %s - %s", iF->name, iV[i].field->name);
6224 else if (0 < iV[i].valid)
6225 D("Already validated %s - %s", iF->name, iV[i].field->name);
6226 }
6227 }
6228 doit = Rd->doit;
6229 form = Rd->form;
6230 }
6231
6232 sessionStateEngine(Rd, "validated");
6233
6234 // Submit the data. Reload input form and sub in case things got changed by the validation functions.
6235 iF = accountPages->get(accountPages, Rd->form, NULL, false);
6236 sub = iF->subs->get(iF->subs, Rd->doit, NULL, false);
6237//if (strcmp("POST", Rd->Method) == 0) // Not such a good idea, since we are faking a submit for account validation, which is a GET.
6238{
6239 if (0 == e)
6240 {
6241 D("For page %s, start of %s submission.", iF->name, sub->name);
6242 if (NULL != sub->submit)
6243 e += sub->submit(Rd, iF, iV);
6244 }
6245}
6246 free(iV);
6247
6248 }
6249 else
6250 {
6251 sessionStateEngine(Rd, "CANCELLED");
6252 }
6253
6254 sessionStateEngine(Rd, "submited");
6255 switch (Rd->shs.status)
6256 {
6257 case SHS_RENEW:
6258 case SHS_REFRESH:
6259 {
6260 freeSesh(Rd, FALSE, FALSE);
6261 newSesh(Rd, FALSE);
6262 break;
6263 }
6264
6265 case SHS_VALID:
6266 case SHS_KEEP:
6267 {
6268 // Do nothing here.
6269 break;
6270 }
6271
6272 case SHS_LOGIN:
6273 case SHS_WIPE:
6274 {
6275// TODO - should wipe the old one, and create this new one with the users UUID.
6276// I think that's what we are doing anyway.
6277 freeSesh(Rd, FALSE, FALSE);
6278 newSesh(Rd, FALSE);
6279 break;
6280 }
6281
6282// TODO - these three should store state, so they can go back to where the user was (or where they where going) before asking them to confirm their login credentials.
6283 case SHS_IDLE:
6284 case SHS_SECURITY:
6285 case SHS_RELOGIN:
6286
6287 case SHS_UNKNOWN:
6288 case SHS_NONE:
6289 case SHS_BOGUS:
6290 case SHS_PROBLEM:
6291 case SHS_ANCIENT:
6292 case SHS_NUKE:
6293 {
6294 freeSesh(Rd, FALSE, TRUE); // Wipe mode clears out all of Rd->database, selected Rd->stuff, and the above commented out Rd->shs.
6295 newSesh(Rd, FALSE);
6296 form = "accountLogin";
6297 doit = "logout";
6298 Rd->doit = doit;
6299 Rd->form = form;
6300 iF = accountPages->get(accountPages, Rd->form, NULL, false);
6301 sub = iF->subs->get(iF->subs, Rd->doit, NULL, false);
6302 Rd->output = sub->outputForm;
6303 break;
6304 }
6305 }
6306
6307 // Return the result.
6308 if (0 == e)
6309 {
6310 if (strcmp("GET", Rd->Method) == 0)
6311 {
6312 // Find the output form.
6313 inputForm *oF = accountPages->get(accountPages, Rd->output, NULL, false);
6314 if (NULL == oF)
6315 {
6316 E("No such account page - %s", Rd->output);
6317 form = "accountLogin";
6318 doit = "logout";
6319 oF = accountPages->get(accountPages, form, NULL, false);
6320 }
6321 D("Building output page %s", oF->name);
6322 count = oF->fields->size(oF->fields);
6323 iV = xzalloc(count * sizeof(inputValue));
6324 collectFields(Rd, oF, iV, -1);
6325 collectFields(Rd, oF, iV, 3);
6326 oF->web(Rd, oF, iV);
6327 free(iV);
6328 }
6329 else
6330 {
6331 // Redirect to a GET if it was a POST.
6332 // The reason here is to avoid a refresh turning into a rePOST.
6333 if ((strcmp("POST", Rd->Method) == 0))
6334 {
6335 if ('\0' != Rd->form[0])
6336 setCookie(Rd, "form", Rd->form);
6337 if ('\0' != Rd->doit[0])
6338 setCookie(Rd, "doit", Rd->doit);
6339 Rd->Rheaders->putstr (Rd->Rheaders, "Status", "303 See Other");
6340 Rd->Rheaders->putstrf(Rd->Rheaders, "Location", "https://%s%s%s", Rd->Host, Rd->RUri, Rd->outQuery);
6341 Rd->reply->addstrf(Rd->reply, "<html><title>Post POST redirect</title><head>"
6342 "<meta http-equiv='refresh' content='0; URL=https://%s%s%s' />"
6343 "</head><body>You should get redirected to <a href='https://%s%s%s'>https://%s%s%s</a></body></html>",
6344 Rd->Host, Rd->RUri, Rd->outQuery, Rd->Host, Rd->RUri, Rd->outQuery, Rd->Host, Rd->RUri, Rd->outQuery
6345 );
6346 I("Redirecting dynamic page %s -> https://%s%s%s (%s)", file, Rd->Host, Rd->RUri, Rd->outQuery, Rd->form);
6347 }
6348 }
6349 }
6350 else
6351 {
6352 if (0 < e)
6353 E("Building output ERROR page %s, coz of %d errors.", iF->name, e);
6354 else
6355 D("Building alternate output page %s", iF->name);
6356 // First just sort out input groups, then get the data from Rd->stuff.
6357 count = iF->fields->size(iF->fields);
6358 iV = xzalloc(count * sizeof(inputValue));
6359 collectFields(Rd, iF, iV, -1);
6360 collectFields(Rd, iF, iV, 3);
6361 iF->eWeb(Rd, iF, iV);
6362 free(iV);
6363 }
6364
6365 free(Rd->outQuery);
6366 Rd->outQuery = NULL;
6367
6368 C("Ending dynamic page %s %s", Rd->RUri, form);
6369}
6370
6371
6372static void cleanup(void)
6373{
6374// TODO - not sure why, but this gets called twice on quitting sometimes.
6375 C("Caught signal, or quitting, cleaning up.");
6376
6377 if (accountPages)
6378 {
6379 qhashtbl_obj_t obj;
6380
6381 memset((void*)&obj, 0, sizeof(obj));
6382 accountPages->lock(accountPages);
6383 while(accountPages->getnext(accountPages, &obj, false) == true)
6384 {
6385 inputForm *f = (inputForm *) obj.data;
6386
6387 f->subs->free(f->subs);
6388 qlisttbl_obj_t fobj;
6389
6390 memset((void *) &fobj, 0, sizeof(fobj));
6391 f->fields->lock(f->fields);
6392 while(f->fields->getnext(f->fields, &fobj, NULL, false) == true)
6393 {
6394 inputField *fld = (inputField *) fobj.data;
6395
6396 if (LUA_TGROUP == fld->type)
6397 free(fld->group);
6398 }
6399 f->fields->unlock(f->fields);
6400 f->fields->free(f->fields);
6401 }
6402 accountPages->unlock(accountPages);
6403 accountPages->free(accountPages);
6404 accountPages = NULL;
6405 }
6406
6407 if (dynPages) dynPages->free(dynPages);
6408 dynPages = NULL;
6409 if (HTMLfileCache)
6410 {
6411 qhashtbl_obj_t obj;
6412
6413 memset((void*)&obj, 0, sizeof(obj));
6414 HTMLfileCache->lock(HTMLfileCache);
6415 while(HTMLfileCache->getnext(HTMLfileCache, &obj, false) == true)
6416 {
6417 HTMLfile *f = (HTMLfile *) obj.data;
6418
6419 unfragize(f->fragments, NULL, TRUE);
6420 }
6421 HTMLfileCache->unlock(HTMLfileCache);
6422 HTMLfileCache->free(HTMLfileCache);
6423 HTMLfileCache = NULL;
6424 }
6425 if (mimeTypes) mimeTypes->free(mimeTypes);
6426 mimeTypes = NULL;
6427 freeDb(true);
6428 if (L) lua_close(L);
6429 L = NULL;
6430 if (stats)
6431 {
6432 if (stats->stats) stats->stats->free(stats->stats);
6433 free(stats);
6434 stats = NULL;
6435 }
6436 if (configs) configs->free(configs);
6437 configs = NULL;
6438}
6439
6440
6441void sledjchisl_main(void)
6442{
6443 char *cmd = *toys.optargs;
6444 char *tmp;
6445 struct stat statbuf;
6446 int status, result, i;
6447 void *vd;
6448
6449 configs = qhashtbl(0, 0);
6450 L = luaL_newstate();
6451
6452 I("libfcgi version: %s", FCGI_VERSION);
6453 I("Lua version: %s", LUA_RELEASE);
6454 I("LuaJIT version: %s", LUAJIT_VERSION);
6455 I("MariaDB / MySQL client version: %s", mysql_get_client_info());
6456 I("OpenSSL version: %s", OPENSSL_VERSION_TEXT);
6457 I("qLibc version: qLibc only git tags for version numbers. Sooo, 2.4.4, unless I forgot to update this.");
6458 I("toybox version: %s", TOYBOX_VERSION);
6459
6460 dbRequests = qlist(0);
6461 sigatexit(cleanup);
6462
6463 pwd = getcwd(0, 0);
6464
6465 if (-1 == fstat(STDIN_FILENO, &statbuf))
6466 {
6467 error_msg("fstat() failed");
6468 if (1 != isatty(STDIN_FILENO))
6469 isWeb = TRUE;
6470 }
6471 else
6472 {
6473 if (S_ISREG (statbuf.st_mode)) D("STDIN is a regular file.");
6474 else if (S_ISDIR (statbuf.st_mode)) D("STDIN is a directory.");
6475 else if (S_ISCHR (statbuf.st_mode)) D("STDIN is a character device.");
6476 else if (S_ISBLK (statbuf.st_mode)) D("STDIN is a block device.");
6477 else if (S_ISFIFO(statbuf.st_mode)) D("STDIN is a FIFO (named pipe).");
6478 else if (S_ISLNK (statbuf.st_mode)) D("STDIN is a symbolic link.");
6479 else if (S_ISSOCK(statbuf.st_mode)) D("STDIN is a socket.");
6480 else D("STDIN is a unknown file descriptor type.");
6481 if (!S_ISCHR(statbuf.st_mode))
6482 isWeb = TRUE;
6483 }
6484
6485 // Print our user name and groups.
6486 struct passwd *pw;
6487 struct group *grp;
6488 uid_t euid = geteuid();
6489 gid_t egid = getegid();
6490 gid_t *groups = (gid_t *)toybuf;
6491 i = sizeof(toybuf)/sizeof(gid_t);
6492 int ngroups;
6493
6494 pw = xgetpwuid(euid);
6495 I("Running as user %s", pw->pw_name);
6496
6497 grp = xgetgrgid(egid);
6498 ngroups = getgroups(i, groups);
6499 if (ngroups < 0) perror_exit("getgroups");
6500 D("User is in group %s", grp->gr_name);
6501 for (i = 0; i < ngroups; i++) {
6502 if (groups[i] != egid)
6503 {
6504 if ((grp = getgrgid(groups[i])))
6505 D("User is in group %s", grp->gr_name);
6506 else
6507 D("User is in group %u", groups[i]);
6508 }
6509 }
6510
6511
6512/* From http://luajit.org/install.html -
6513To change or extend the list of standard libraries to load, copy
6514src/lib_init.c to your project and modify it accordingly. Make sure the
6515jit library is loaded or the JIT compiler will not be activated.
6516*/
6517 luaL_openlibs(L); // Load Lua libraries.
6518
6519 // Load the config scripts.
6520 char *cPaths[] =
6521 {
6522 ".sledjChisl.conf.lua",
6523 "/etc/sledjChisl.conf.lua",
6524// "/etc/sledjChisl.d/*.lua",
6525 "~/.sledjChisl.conf.lua",
6526// "~/.config/sledjChisl/*.lua",
6527 NULL
6528 };
6529 struct stat st;
6530
6531
6532 for (i = 0; cPaths[i]; i++)
6533 {
6534 memset(toybuf, 0, sizeof(toybuf));
6535 if (('/' == cPaths[i][0]) || ('~' == cPaths[i][0]))
6536 snprintf(toybuf, sizeof(toybuf), "%s", cPaths[i]);
6537 else
6538 snprintf(toybuf, sizeof(toybuf), "%s/%s", pwd, cPaths[i]);
6539 if (0 != lstat(toybuf, &st))
6540 continue;
6541 I("Loading configuration file - %s", toybuf);
6542 status = luaL_loadfile(L, toybuf);
6543 if (status) // If something went wrong, error message is at the top of the stack.
6544 E("Couldn't load file: %s", lua_tostring(L, -1));
6545 else
6546 {
6547 result = lua_pcall(L, 0, LUA_MULTRET, 0);
6548 if (result)
6549 E("Failed to run script: %s", lua_tostring(L, -1));
6550 else
6551 {
6552 lua_getglobal(L, "config");
6553 lua_pushnil(L);
6554 while(lua_next(L, -2) != 0)
6555 {
6556 char *n = (char *) lua_tostring(L, -2);
6557
6558 // Numbers can convert to strings, so check for numbers before checking for strings.
6559 // On the other hand, strings that can be converted to numbers also pass lua_isnumber(). sigh
6560 if (lua_isnumber(L, -1))
6561 {
6562 float v = lua_tonumber(L, -1);
6563 configs->put(configs, n, &v, sizeof(float));
6564 }
6565 else if (lua_isstring(L, -1))
6566 configs->putstr(configs, n, (char *) lua_tostring(L, -1));
6567 else if (lua_isboolean(L, -1))
6568 {
6569 int v = lua_toboolean(L, -1);
6570 configs->putint(configs, n, v);
6571 }
6572 else
6573 {
6574 char *v = (char *) lua_tostring(L, -1);
6575 E("Unknown config variable type for %s = %s", n, v);
6576 }
6577 lua_pop(L, 1);
6578 }
6579 }
6580 }
6581 }
6582 DEBUG = configs->getint(configs, "debug");
6583 D("Setting DEBUG = %d", DEBUG);
6584 if ((vd = configs->get (configs, "loadAverageInc", NULL, false)) != NULL) {loadAverageInc = *((float *) vd); D("Setting loadAverageInc = %f", loadAverageInc);}
6585 if ((vd = configs->get (configs, "simTimeOut", NULL, false)) != NULL) {simTimeOut = (int) *((float *) vd); D("Setting simTimeOut = %d", simTimeOut);}
6586 if ((tmp = configs->getstr(configs, "scRoot", false)) != NULL) {scRoot = tmp; D("Setting scRoot = %s", scRoot);}
6587 if ((tmp = configs->getstr(configs, "scUser", false)) != NULL) {scUser = tmp; D("Setting scUser = %s", scUser);}
6588 if ((tmp = configs->getstr(configs, "Tconsole", false)) != NULL) {Tconsole = tmp; D("Setting Tconsole = %s", Tconsole);}
6589 if ((tmp = configs->getstr(configs, "Tsocket", false)) != NULL) {Tsocket = tmp; D("Setting Tsocket = %s", Tsocket);}
6590 if ((tmp = configs->getstr(configs, "Ttab", false)) != NULL) {Ttab = tmp; D("Setting Ttab = %s", Ttab);}
6591 if ((tmp = configs->getstr(configs, "webRoot", false)) != NULL) {webRoot = tmp; D("Setting webRoot = %s", webRoot);}
6592 if ((tmp = configs->getstr(configs, "URL", false)) != NULL) {URL = tmp; D("Setting URL = %s", URL);}
6593 if ((vd = configs->get (configs, "seshRenew", NULL, false)) != NULL) {seshRenew = (int) *((float *) vd); D("Setting seshRenew = %d", seshRenew);}
6594 if ((vd = configs->get (configs, "idleTimeOut", NULL, false)) != NULL) {idleTimeOut = (int) *((float *) vd); D("Setting idleTimeOut = %d", idleTimeOut);}
6595 if ((vd = configs->get (configs, "seshTimeOut", NULL, false)) != NULL) {seshTimeOut = (int) *((float *) vd); D("Setting seshTimeOut = %d", seshTimeOut);}
6596 if ((vd = configs->get (configs, "newbieTimeOut", NULL, false)) != NULL) {newbieTimeOut = (int) *((float *) vd); D("Setting newbieTimeOut = %d", newbieTimeOut);}
6597 if ((tmp = configs->getstr(configs, "ToS", false)) != NULL) {ToS = tmp; D("Setting ToS = %s", ToS);}
6598 if ((tmp = configs->getstr(configs, "webIframers", false)) != NULL) {webIframers = tmp; D("Setting webIframers = %s", webIframers);}
6599
6600
6601 // Use a FHS compatible setup -
6602 if (strcmp("/", scRoot) == 0)
6603 {
6604 scBin = "/bin";
6605 scEtc = "/etc/opensim_SC";
6606 scLib = "/usr/lib/opensim_SC";
6607 scRun = "/run/opensim_SC";
6608 scBackup = "/var/backups/opensim_SC";
6609 scCache = "/var/cache/opensim_SC";
6610 scData = "/var/lib/opensim_SC";
6611 scLog = "/var/log/opensim_SC";
6612 }
6613 else if (strcmp("/usr/local", scRoot) == 0)
6614 {
6615 scBin = "/usr/local/bin";
6616 scEtc = "/usr/local/etc/opensim_SC";
6617 scLib = "/usr/local/lib/opensim_SC";
6618 scRun = "/run/opensim_SC";
6619 scBackup = "/var/local/backups/opensim_SC";
6620 scCache = "/var/local/cache/opensim_SC";
6621 scData = "/var/local/lib/opensim_SC";
6622 scLog = "/var/local/log/opensim_SC";
6623 }
6624 else // A place for everything to live, like /opt/opensim_SC
6625 {
6626 char *slsh = "";
6627
6628 if ('/' != scRoot[0])
6629 {
6630 E("scRoot is not an absolute path - %s.", scRoot);
6631 slsh = "/";
6632 }
6633 // The problem here is that OpenSim already uses /opt/opensim_SC/current/bin for all of it's crap, where they mix everything together.
6634 // Don't want it in the path.
6635 // Could put the .dlls into lib. Most of the rest is config files or common assets.
6636 // Or just slowly migrate opensim stuff to FHS.
6637 scBin = xmprintf("%s%s/bin", slsh, scRoot);
6638 scEtc = xmprintf("%s%s/etc", slsh, scRoot);
6639 scLib = xmprintf("%s%s/lib", slsh, scRoot);
6640 scRun = xmprintf("%s%s/var/run", slsh, scRoot);
6641 scBackup = xmprintf("%s%s/var/backups", slsh, scRoot);
6642 scCache = xmprintf("%s%s/var/cache", slsh, scRoot);
6643 scData = xmprintf("%s%s/var/lib", slsh, scRoot);
6644 scLog = xmprintf("%s%s/var/log", slsh, scRoot);
6645 }
6646
6647
6648// TODO - still a bit chicken and egg here about the tmux socket and reading configs from scEtc /.sledjChisl.conf.lua
6649 if (!isWeb)
6650 {
6651 I("Outputting to a terminal, not a web server.");
6652 // Check if we are already running inside the proper tmux server.
6653 char *eTMUX = getenv("TMUX");
6654 memset(toybuf, 0, sizeof(toybuf));
6655 snprintf(toybuf, sizeof(toybuf), "%s/%s", scRun, Tsocket);
6656 if (((eTMUX) && (0 == strncmp(toybuf, eTMUX, strlen(toybuf)))))
6657 {
6658 I("Running inside the proper tmux server. %s", eTMUX);
6659 isTmux = TRUE;
6660 }
6661 else
6662 I("Not running inside the proper tmux server, starting it. %s == %s", eTMUX, toybuf);
6663 }
6664
6665 if (isTmux || isWeb)
6666 {
6667 char *d;
6668
6669 // Doing this here coz at this point we should be the correct user.
6670 if ((! qfile_exist(scBin)) && (! qfile_mkdir(scBin, S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP, true))) C("Unable to create path %s", scBin);
6671 if ((! qfile_exist(scEtc)) && (! qfile_mkdir(scEtc, S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP, true))) C("Unable to create path %s", scEtc);
6672 if ((! qfile_exist(scLib)) && (! qfile_mkdir(scLib, S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP, true))) C("Unable to create path %s", scLib);
6673 if ((! qfile_exist(scRun)) && (! qfile_mkdir(scRun, S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IWGRP | S_IXGRP | S_ISGID, true))) C("Unable to create path %s", scRun);
6674 if ((! qfile_exist(scBackup)) && (! qfile_mkdir(scBackup, S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP, true))) C("Unable to create path %s", scBackup);
6675// TODO - the path to scCache/sledjchisl.socket needs to be readable by the www-data group. So the FCGI socket will work.
6676// AND it needs to be group sticky on opensimsc group. So the tmux socket will work.
6677// So currently scCache is www-data readable, and scRun is group sticky.
6678 if ((! qfile_exist(scCache)) && (! qfile_mkdir(scCache, S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP, true))) C("Unable to create path %s", scCache);
6679 if ((! qfile_exist(scData)) && (! qfile_mkdir(scData, S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP, true))) C("Unable to create path %s", scData);
6680 if ((! qfile_exist(scLog)) && (! qfile_mkdir(scLog, S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP, true))) C("Unable to create path %s", scLog);
6681 tmp = xmprintf("%s/sessions", scCache);
6682 if ((! qfile_exist(tmp)) && (! qfile_mkdir(tmp, S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP, true))) C("Unable to create path %s", tmp);
6683 free(tmp);
6684 tmp = xmprintf("%s/users", scData);
6685 if ((! qfile_exist(tmp)) && (! qfile_mkdir(tmp, S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP, true))) C("Unable to create path %s", tmp);
6686 free(tmp);
6687
6688
6689 mimeTypes = qhashtbl(0, 0);
6690 mimeTypes->putstr(mimeTypes, "gz", "application/gzip");
6691 mimeTypes->putstr(mimeTypes, "js", "application/javascript");
6692 mimeTypes->putstr(mimeTypes, "json", "application/json");
6693 mimeTypes->putstr(mimeTypes, "pdf", "application/pdf");
6694 mimeTypes->putstr(mimeTypes, "rtf", "application/rtf");
6695 mimeTypes->putstr(mimeTypes, "zip", "application/zip");
6696 mimeTypes->putstr(mimeTypes, "xz", "application/x-xz");
6697 mimeTypes->putstr(mimeTypes, "gif", "image/gif");
6698 mimeTypes->putstr(mimeTypes, "png", "image/png");
6699 mimeTypes->putstr(mimeTypes, "jp2", "image/jp2");
6700 mimeTypes->putstr(mimeTypes, "jpg2", "image/jp2");
6701 mimeTypes->putstr(mimeTypes, "jpe", "image/jpeg");
6702 mimeTypes->putstr(mimeTypes, "jpg", "image/jpeg");
6703 mimeTypes->putstr(mimeTypes, "jpeg", "image/jpeg");
6704 mimeTypes->putstr(mimeTypes, "svg", "image/svg+xml");
6705 mimeTypes->putstr(mimeTypes, "svgz", "image/svg+xml");
6706 mimeTypes->putstr(mimeTypes, "tif", "image/tiff");
6707 mimeTypes->putstr(mimeTypes, "tiff", "image/tiff");
6708 mimeTypes->putstr(mimeTypes, "css", "text/css");
6709 mimeTypes->putstr(mimeTypes, "html", "text/html");
6710 mimeTypes->putstr(mimeTypes, "htm", "text/html");
6711 mimeTypes->putstr(mimeTypes, "shtml", "text/html");
6712// mimeTypes->putstr(mimeTypes, "md", "text/markdown");
6713// mimeTypes->putstr(mimeTypes, "markdown", "text/markdown");
6714 mimeTypes->putstr(mimeTypes, "txt", "text/plain");
6715
6716 memset(toybuf, 0, sizeof(toybuf));
6717 snprintf(toybuf, sizeof(toybuf), "%s/config/config.ini", scRoot);
6718
6719// TODO - it looks like OpenSim invented their own half arsed backwards INI file include system.
6720// I doubt qLibc supports it, like it supports what seems to be the standard include system.
6721// Not sure if we need to worry about it just yet.
6722// TODO - this leaks memory, but it's a bug in qLibc. Send the bug fix upstream.
6723 qlisttbl_t *qconfig = qconfig_parse_file(NULL, toybuf, '=');
6724 if (NULL == qconfig)
6725 {
6726 E("Can't read config file %s", toybuf);
6727 goto finished;
6728 }
6729 d = qstrunchar(qconfig->getstr(qconfig, "Const.ConnectionString", false), '"', '"');
6730
6731 if (NULL == d)
6732 {
6733 E("No database credentials in %s!", toybuf);
6734 goto finished;
6735 }
6736 else
6737 {
6738 char *p0, *p1, *p2;
6739 if (NULL == (d = strdup(d)))
6740 {
6741 E("Out of memory!");
6742 goto finished;
6743 }
6744 // Data Source=MYSQL_HOST;Database=MYSQL_DB;User ID=MYSQL_USER;Password=MYSQL_PASSWORD;Old Guids=true;
6745 p0 = d;
6746 while (NULL != p0)
6747 {
6748 p1 = strchr(p0, '=');
6749 if (NULL == p1) break;
6750 *p1 = '\0';
6751 p2 = strchr(p1 + 1, ';');
6752 if (NULL == p2) break;
6753 *p2 = '\0';
6754 configs->putstr(configs, p0, p1 + 1); // NOTE - this allocs memory for it's key and it's data.
6755 p0 = p2 + 1;
6756 if ('\0' == *p0)
6757 p0 = NULL;
6758 };
6759 free(d);
6760 }
6761
6762// TODO - should also load god names, and maybe the SMTP stuff.
6763// Note that the OpenSim SMTP stuff is ONLY for LSL script usage, we probably want to put it in the Lua config file instead.
6764
6765 if (mysql_library_init(toys.optc, toys.optargs, NULL))
6766 {
6767 E("mysql_library_init() failed!");
6768 goto finished;
6769 }
6770 if (!dbConnect())
6771 goto finished;
6772
6773 // Need to kick this off.
6774 stats = getStats(database, stats);
6775 char *h = qstrunchar(qconfig->getstr(qconfig, "Const.HostName", false), '"', '"');
6776 char *p = qstrunchar(qconfig->getstr(qconfig, "Const.PublicPort", false), '"', '"');
6777 stats->stats->putstr(stats->stats, "grid", qstrunchar(qconfig->getstr(qconfig, "Const.GridName", false), '"', '"'));
6778 stats->stats->putstr(stats->stats, "HostName", h);
6779 stats->stats->putstr(stats->stats, "PublicPort", p);
6780 snprintf(toybuf, sizeof(toybuf), "http://%s:%s/", h, p);
6781
6782 stats->stats->putstr(stats->stats, "uri", toybuf);
6783 qconfig->free(qconfig);
6784 }
6785
6786
6787 if (isWeb)
6788 {
6789 char **initialEnv = environ;
6790 char *tmp0, *tmp1;
6791 int count = 0, entries, bytes;
6792
6793 dynPages = qhashtbl(0, 0);
6794 newDynPage("account.html", (pageFunction) account_html);
6795
6796 // FCGI_LISTENSOCK_FILENO is the socket to the web server.
6797 // STDOUT and STDERR go to the web servers error log, or at least it does in Apache 2 mod_fcgid.
6798 I("Running SledjChisl inside a web server, pid %d.", getpid());
6799
6800 if (0 == toys.optc)
6801 D("no args");
6802 else
6803 {
6804 for (entries = 0, bytes = -1; entries < toys.optc; entries++)
6805 D("ARG %s\n", toys.optargs[entries]);
6806 }
6807 printEnv(environ);
6808
6809/*
6810? https://stackoverflow.com/questions/30707792/how-to-disable-buffering-with-apache2-and-mod-proxy-fcgi
6811 https://z-issue.com/wp/apache-2-4-the-event-mpm-php-via-mod_proxy_fcgi-and-php-fpm-with-vhosts/
6812 A lengthy and detailed "how to set this up with PHP" that might be useful.
6813 https://www.apachelounge.com/viewtopic.php?t=4385
6814 "Also the new mod_proxy_fcgi for Apache 2.4 seems to be crippled just like mod_fcgid in terms of being limited to just one request per process at a time."
6815 But then so is the silly fcgi2 SDK, which basically assumes it's a CGI wrapper, not proper FCGI.
6816+ I could even start the spawn-fcgi process from the tmux instance of sledjchisl.
6817+ Orrr just open the socket / port myself from the tmux instance and do the raw FCGI thing through it.
6818*/
6819 while (FCGI_Accept() != -1)
6820 {
6821 reqData *Rd = xzalloc(sizeof(reqData));
6822
6823 if (-1 == clock_gettime(CLOCK_REALTIME, &Rd->then))
6824 perror_msg("Unable to get the time.");
6825 Rd->L = L;
6826 Rd->configs = configs;
6827// Rd->queries = qhashtbl(0, 0); // Inited in toknize below.
6828// Rd->body = qhashtbl(0, 0); // Inited in toknize below.
6829// Rd->cookies = qhashtbl(0, 0); // Inited in toknize below.
6830 Rd->headers = qhashtbl(0, 0);
6831 Rd->valid = qhashtbl(0, 0);
6832 Rd->stuff = qhashtbl(0, 0);
6833 Rd->database = qhashtbl(0, 0);
6834 Rd->Rcookies = qhashtbl(0, 0);
6835 Rd->Rheaders = qhashtbl(0, 0);
6836 Rd->stats = stats;
6837 Rd->errors = qlist(0);
6838 Rd->messages = qlist(0);
6839 Rd->reply = qgrow(QGROW_THREADSAFE);
6840 Rd->outQuery = xstrdup("");
6841 Rd->shs.status = SHS_UNKNOWN;
6842 qhashtbl_obj_t hobj;
6843 qlist_obj_t lobj;
6844
6845 // So far I'm seeing these as all caps, but I don't think they are defined that way. Case insensitive per the spec.
6846 // So convert them now, also "-" -> "_".
6847t("HEADERS");
6848 char **envp = environ;
6849 for ( ; *envp != NULL; envp++)
6850 {
6851 char *k = xstrdup(*envp);
6852 char *v = strchr(k, '=');
6853
6854 if (NULL != v)
6855 {
6856 *v = '\0';
6857 char *ky = qstrreplace("tr", qstrupper(k), "-", "_");
6858 Rd->headers->putstr(Rd->headers, ky, v + 1);
6859if ((strcmp("HTTP_COOKIE", ky) == 0) || (strcmp("CONTENT_LENGTH", ky) == 0) || (strcmp("QUERY_STRING", ky) == 0))
6860 d(" %s = %s", ky, v + 1);
6861 }
6862 free(k);
6863 }
6864
6865 // The FCGI paramaters sent from the server, are converted to environment variablse for the fcgi2 SDK.
6866 // The FCGI spec doesn't mention what these are. except FCGI_WEB_SERVER_ADDRS.
6867 char *Role = Rd->headers->getstr(Rd->headers, "FCGI_ROLE", false);
6868 Rd->Path = Rd->headers->getstr(Rd->headers, "PATH_INFO", false);
6869// if (NULL == Rd->Path) {msleep(1000); continue;}
6870 char *Length = Rd->headers->getstr(Rd->headers, "CONTENT_LENGTH", false);
6871//char *Type = Rd->headers->getstr(Rd->headers, "CONTENT_TYPE", false);
6872 Rd->Method = Rd->headers->getstr(Rd->headers, "REQUEST_METHOD", false);
6873 Rd->Script = Rd->headers->getstr(Rd->headers, "SCRIPT_NAME", false);
6874 Rd->Scheme = Rd->headers->getstr(Rd->headers, "REQUEST_SCHEME", false);
6875 Rd->Host = Rd->headers->getstr(Rd->headers, "HTTP_HOST", false);
6876//char *SUri = Rd->headers->getstr(Rd->headers, "SCRIPT_URI", false);
6877 Rd->RUri = Rd->headers->getstr(Rd->headers, "REQUEST_URI", true);
6878//char *Cookies = Rd->headers->getstr(Rd->headers, "HTTP_COOKIE", false);
6879//char *Referer = Rd->headers->getstr(Rd->headers, "HTTP_REFERER", false);
6880//char *RAddr = Rd->headers->getstr(Rd->headers, "REMOTE_ADDR", false);
6881//char *Cache = Rd->headers->getstr(Rd->headers, "HTTP_CACHE_CONTROL", false);
6882//char *FAddrs = Rd->headers->getstr(Rd->headers, "FCGI_WEB_SERVER_ADDRS", false);
6883//char *Since = Rd->headers->getstr(Rd->headers, "IF_MODIFIED_SINCE", false);
6884 /* Per the spec CGI https://www.ietf.org/rfc/rfc3875
6885 meta-variable-name = "AUTH_TYPE" | "CONTENT_LENGTH" |
6886 "CONTENT_TYPE" | "GATEWAY_INTERFACE" |
6887 "PATH_INFO" | "PATH_TRANSLATED" |
6888 "QUERY_STRING" | "REMOTE_ADDR" |
6889 "REMOTE_HOST" | "REMOTE_IDENT" |
6890 "REMOTE_USER" | "REQUEST_METHOD" |
6891 "SCRIPT_NAME" | "SERVER_NAME" |
6892 "SERVER_PORT" | "SERVER_PROTOCOL" |
6893 "SERVER_SOFTWARE"
6894 Also protocol / scheme specific ones -
6895 HTTP_* comes from some of the request header. The rest are likely part of the other env variables.
6896 Also covers HTTPS headers, with the HTTP_* names.
6897
6898 */
6899
6900t("COOKIES");
6901 Rd->cookies = toknize(Rd->headers->getstr(Rd->headers, "HTTP_COOKIE", false), "=;");
6902 santize(Rd->cookies);
6903 if (strcmp("GET", Rd->Method) == 0)
6904 { // In theory a POST has body fields INSTEAD of query fields. Especially for ignoring the linky-hashish after the validation page.
6905t("QUERY");
6906 Rd->queries = toknize(Rd->headers->getstr(Rd->headers, "QUERY_STRING", false), "=&");
6907 santize(Rd->queries);
6908 }
6909 else
6910 {
6911T("ignoring QUERY");
6912 Rd->queries = qhashtbl(0, 0);
6913 free(Rd->RUri);
6914 Rd->RUri = xmprintf("%s%s", Rd->Script, Rd->Path);
6915 }
6916t("BODY");
6917 char *Body = NULL;
6918 if (Length != NULL)
6919 {
6920 size_t len = strtol(Length, NULL, 10);
6921 Body = xmalloc(len + 1);
6922 int c = FCGI_fread(Body, 1, len, FCGI_stdin);
6923 if (c != len)
6924 {
6925 E("Tried to read %d of the body, only got %d!", len, c);
6926 }
6927 Body[len] = '\0';
6928 }
6929 else
6930 Length = "0";
6931 Rd->body = toknize(Body, "=&");
6932 free(Body);
6933 santize(Rd->body);
6934
6935 I("%s %s://%s%s -> %s%s", Rd->Method, Rd->Scheme, Rd->Host, Rd->RUri, webRoot, Rd->Path);
6936 D("Started FCGI web request ROLE = %s, body is %s bytes, pid %d.", Role, Length, getpid());
6937
6938 if (NULL == Rd->Path)
6939 {
6940 E("NULL path in FCGI request!");
6941 Rd->Rheaders->putstr(Rd->Rheaders, "Status", "404 Not Found");
6942 goto sendReply;
6943 }
6944
6945/* TODO - other headers may include -
6946 different Content-type
6947 Status: 304 Not Modified
6948 Last-Modified: timedatemumble
6949 https://en.wikipedia.org/wiki/List_of_HTTP_header_fields
6950*/
6951 Rd->Rheaders->putstr(Rd->Rheaders, "Status", "200 OK");
6952 Rd->Rheaders->putstr(Rd->Rheaders, "Content-type", "text/html");
6953// TODO - check these.
6954 // This is all dynamic web pages, and likeley secure to.
6955 // Most of these are from https://www.smashingmagazine.com/2017/04/secure-web-app-http-headers/
6956 // https://www.twilio.com/blog/a-http-headers-for-the-responsible-developer is good to, and includes useful compression and image stuff.
6957 // On the other hand, .css files are referenced, which might be better off being cached, so should tweak some of thees.
6958 Rd->Rheaders->putstr(Rd->Rheaders, "Cache-Control", "no-cache, no-store, must-revalidate");
6959 Rd->Rheaders->putstr(Rd->Rheaders, "Pragma", "no-cache");
6960 Rd->Rheaders->putstr(Rd->Rheaders, "Expires", "-1");
6961 Rd->Rheaders->putstr(Rd->Rheaders, "Strict-Transport-Security", "max-age=63072000"); // Two years.
6962// TODO - do something about this -
6963 /* https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/style-src
6964 "Note: Disallowing inline styles and inline scripts is one of
6965 the biggest security wins CSP provides. However, if you
6966 absolutely have to use it, there are a few mechanisms that
6967 will allow them."
6968
6969 WTF? And the mechanisms include nonces, hashes, or 'unsafe-inline'.
6970 Not sure why inline styles need to be that secure, when downloaded ones are not.
6971 Ah, it's for user input that is sent back to other users, they might include funky CSS in their input.
6972 SOOOO, proper validation and escaping is needed.
6973 OOOOR, use the nonce, and make it a different nonce per page serve.
6974 OOOOR, just put all the style stuff in a .css file. Then we can use style-src 'self' without the 'unsafe-inline'?
6975 There's only one block of <style in the header I think.
6976 */
6977 // Content-Security-Policy can get complex, and I first wrote that when it was very simple. lol
6978 if ('\0' != webIframers[0])
6979 Rd->Rheaders->putstrf(Rd->Rheaders, "Content-Security-Policy",
6980 "default-src 'self'; script-src 'none'; form-action 'self'; style-src 'self' 'unsafe-inline'; frame-ancestors 'self' %s", webIframers);
6981 else
6982 {
6983 Rd->Rheaders->putstr(Rd->Rheaders, "Content-Security-Policy", "default-src 'self'; script-src 'none'; form-action 'self'; style-src 'self' 'unsafe-inline'");
6984 Rd->Rheaders->putstr(Rd->Rheaders, "X-Frame-Options", "SAMEORIGIN"); // This is deprecated, and is an all or nothing thing.
6985 }
6986 Rd->Rheaders->putstr(Rd->Rheaders, "X-XSS-Protection", "1;mode=block");
6987 Rd->Rheaders->putstr(Rd->Rheaders, "X-Content-Type-Options", "nosniff");
6988// Failed experiment, looks like JavaScript is the only way to change headers for the session ID.
6989// Rd->Rheaders->putstr(Rd->Rheaders, "X-Toke-N-Munchie", "foo, bar");
6990
6991 if ((strcmp("GET", Rd->Method) != 0) && (strcmp("HEAD", Rd->Method) != 0) && (strcmp("POST", Rd->Method) != 0))
6992 {
6993 E("Unsupported HTTP method %s", Rd->Method);
6994 Rd->Rheaders->putstr(Rd->Rheaders, "Status", "405 Method Not Allowed");
6995 goto sendReply;
6996 }
6997
6998 memset(toybuf, 0, sizeof(toybuf));
6999 snprintf(toybuf, sizeof(toybuf), "%s%s", webRoot, Rd->Path);
7000 HTMLfile *thisFile = checkHTMLcache(toybuf);
7001 if (NULL == thisFile)
7002 {
7003 dynPage *dp = dynPages->get(dynPages, &Rd->Path[1], NULL, false);
7004 if (NULL == dp)
7005 {
7006 E("Can't access file %s", toybuf);
7007 Rd->Rheaders->putstr(Rd->Rheaders, "Status", "404 Not Found");
7008 E("Failed to open %s, it's not a virtual file either", toybuf);
7009 goto sendReply;
7010 }
7011 dp->func(toybuf, Rd, thisFile);
7012 char *finl = Rd->reply->tostring(Rd->reply); // This mallocs new storage and returns it to us.
7013// TODO - maybe cache this?
7014 qlist_t *fragments = fragize(finl, Rd->reply->datasize(Rd->reply));
7015 Rd->reply->free(Rd->reply);
7016 Rd->reply = qgrow(QGROW_THREADSAFE);
7017 unfragize(fragments, Rd, TRUE);
7018 free(finl);
7019goto sendReply;
7020 }
7021
7022 tmp0 = qfile_get_ext(toybuf);
7023 tmp1 = mimeTypes->getstr(mimeTypes, tmp0, false);
7024 free(tmp0);
7025 if (NULL != tmp1)
7026 {
7027 if (strncmp("text/", tmp1, 5) != 0)
7028 {
7029 E("Only text formats are supported - %s", toybuf);
7030 Rd->Rheaders->putstr(Rd->Rheaders, "Status", "415 Unsupported Media Type");
7031 goto sendReply;
7032 }
7033 }
7034 else
7035 {
7036 E("Not actually a teapot, er I mean file has no extension, can't determine media type the easy way - %s", toybuf);
7037 Rd->Rheaders->putstr(Rd->Rheaders, "Status", "418 I'm a teapot");
7038 goto sendReply;
7039 }
7040
7041// Rd->Rheaders->putstr(Rd->Rheaders, "Last-Modified", thisFile->last.tv_sec);
7042// This is dynamic content, it's always gonna be modified. I think.
7043// if (NULL != Since)
7044// {
7045// time_t snc = qtime_parse_gmtstr(Since);
7046// TODO - should validate the time, log and ignore it if not valid.
7047// if (thisFile->last.tv_sec < snc)
7048// {
7049// D("Status: 304 Not Modified - %s", toybuf);
7050// setHeader("Status", "304 Not Modified");
7051// goto sendReply;
7052// }
7053// }
7054
7055 if (strcmp("HEAD", Rd->Method) == 0)
7056 goto sendReply;
7057
7058 getStats(database, stats);
7059 unfragize(thisFile->fragments, Rd, FALSE);
7060 free(thisFile);
7061
7062sendReply:
7063 /* Send headers.
7064 BTW, the Status header should be sent first I think.
7065 https://www.ietf.org/rfc/rfc3875 6.2 says order isn't important.
7066 It even says Status is optional, 200 is assumed. Content-Type is mandatory.
7067 8.2 "Recommendations for Scripts" is worth complying with.
7068 9 "Security Considerations"
7069 https://tools.ietf.org/html/rfc7230 3.1.2 says status line must be first. lol
7070 */
7071 FCGI_printf("Status: %s\r\n", getStrH(Rd->Rheaders, "Status"));
7072 memset((void *) &hobj, 0, sizeof(hobj));
7073 Rd->Rheaders->lock(Rd->Rheaders);
7074 while (Rd->Rheaders->getnext(Rd->Rheaders, &hobj, false) == true)
7075 {
7076 if (strcmp("Status", (char *) hobj.name) != 0)
7077 FCGI_printf("%s: %s\r\n", (char *) hobj.name, (char *) hobj.data);
7078 }
7079 Rd->Rheaders->unlock(Rd->Rheaders);
7080 // Send cookies.
7081 memset((void *) &hobj, 0, sizeof(hobj));
7082 Rd->Rcookies->lock(Rd->Rcookies);
7083 while (Rd->Rcookies->getnext(Rd->Rcookies, &hobj, false) == true)
7084 {
7085 cookie *ck = (cookie *) hobj.data;
7086 FCGI_printf("Set-Cookie: %s=%s", hobj.name, ck->value);
7087// if (NULL != ck->expires) FCGI_printf("; Expires=%s", ck->expires);
7088 if (NULL != ck->domain) FCGI_printf("; Domain=%s", ck->domain);
7089 if (NULL != ck->path) FCGI_printf("; Path=%s", ck->path);
7090 if (0 != ck->maxAge) FCGI_printf("; Max-Age=%d", ck->maxAge);
7091 if ( ck->secure) FCGI_printf("; Secure");
7092 if ( ck->httpOnly) FCGI_printf("; HttpOnly");
7093 if (CS_STRICT == ck->site) FCGI_printf("; SameSite=Strict");
7094 if (CS_LAX == ck->site) FCGI_printf("; SameSite=Lax");
7095 if (CS_NONE == ck->site) FCGI_printf("; SameSite=None");
7096 FCGI_printf("\r\n");
7097 free(ck->value);
7098 ck->value = NULL;
7099 }
7100 FCGI_printf("\r\n");
7101 Rd->cookies->unlock(Rd->cookies);
7102 // Send body.
7103 char *final = Rd->reply->tostring(Rd->reply);
7104 if (NULL == final)
7105 {
7106 tmp0 = Rd->Rheaders->getstr(Rd->Rheaders, "Status", false);
7107 if (NULL == tmp0)
7108 {
7109 E("Some sort of error happpened! Status: UNKNOWN!!");
7110 FCGI_printf("Some sort of error happpened! Status: UNKNOWN!!");
7111 }
7112 else
7113 {
7114 E("Some sort of error happpened! Status: %s", tmp0);
7115 FCGI_printf("Some sort of error happpened! Status: %s", tmp0);
7116 }
7117 }
7118 else
7119 {
7120 FCGI_printf("%s", final);
7121 free(final);
7122 }
7123
7124fcgiDone:
7125 FCGI_Finish();
7126 if (NULL != Rd->outQuery) free(Rd->outQuery);
7127 if (NULL != Rd->shs.name) free(Rd->shs.name);
7128 Rd->shs.name = NULL;
7129 if (NULL != Rd->shs.UUID) free(Rd->shs.UUID);
7130 Rd->shs.UUID = NULL;
7131 qgrow_free(Rd->reply);
7132 qlist_free(Rd->messages);
7133 qlist_free(Rd->errors);
7134 qhashtbl_free(Rd->Rheaders);
7135 qhashtbl_free(Rd->Rcookies);
7136 qhashtbl_free(Rd->database);
7137 qhashtbl_free(Rd->stuff);
7138 qhashtbl_free(Rd->valid);
7139 qhashtbl_free(Rd->headers);
7140 qhashtbl_free(Rd->cookies);
7141 qhashtbl_free(Rd->body);
7142 qhashtbl_free(Rd->queries);
7143 if (Rd->lnk) free(Rd->lnk);
7144 free(Rd->RUri);
7145
7146 struct timespec now;
7147 if (-1 == clock_gettime(CLOCK_REALTIME, &now))
7148 perror_msg("Unable to get the time.");
7149 double n = (now.tv_sec * 1000000000.0) + now.tv_nsec;
7150 double t = (Rd->then.tv_sec * 1000000000.0) + Rd->then.tv_nsec;
7151 I("Finished web request, took %lf seconds", (n - t) / 1000000000.0);
7152 free(Rd);
7153 }
7154
7155 FCGI_fprintf(FCGI_stderr, "Stopped SledjChisl web server.\n");
7156 D("Stopped SledjChisl web server.");
7157
7158 goto finished;
7159 }
7160
7161
7162 if (!isTmux)
7163 { // Let's see if the proper tmux server is even running.
7164 memset(toybuf, 0, sizeof(toybuf));
7165 snprintf(toybuf, sizeof(toybuf), "%s %s/%s -q list-sessions 2>/dev/null | grep -q %s:", Tcmd, scRun, Tsocket, Tconsole);
7166 i = system(toybuf);
7167 if (WIFEXITED(i))
7168 {
7169 if (0 != WEXITSTATUS(i)) // No such sesion, create it.
7170 {
7171 memset(toybuf, 0, sizeof(toybuf));
7172 // The sudo is only so that the session is owned by opensim, otherwise it's owned by whoever ran this script, which is a likely security hole.
7173 // After the session is created, we rely on the scRun directory to be group sticky, so that anyone in the opensim group can attach to the tmux socket.
7174 snprintf(toybuf, sizeof(toybuf),
7175 "sudo -Hu %s %s %s/%s new-session -d -s %s -n '%s' \\; split-window -bhp 50 -t '%s:' bash -c '/usr/bin/valgrind --leak-check=full ./sledjchisl; cd %s; bash'",
7176 scUser, Tcmd, scRun, Tsocket, Tconsole, Ttab, Tconsole, scRoot);
7177 i = system(toybuf);
7178 if (!WIFEXITED(i))
7179 E("tmux new-session command failed! %s", toybuf);
7180 }
7181 // Join the session.
7182 memset(toybuf, 0, sizeof(toybuf));
7183 snprintf(toybuf, sizeof(toybuf), "%s %s/%s select-window -t '%s' \\; attach-session -t '%s'", Tcmd, scRun, Tsocket, Tconsole, Tconsole);
7184 i = system(toybuf);
7185 if (!WIFEXITED(i))
7186 E("tmux attach-session command failed! %s", toybuf);
7187 goto finished;
7188 }
7189 else
7190 E("tmux list-sessions command failed! %s", toybuf);
7191 }
7192
7193
7194 simList *sims = getSims();
7195 if (1)
7196 {
7197 struct sysinfo info;
7198 float la;
7199
7200 sysinfo(&info);
7201 la = info.loads[0]/65536.0;
7202
7203 if (!checkSimIsRunning("ROBUST"))
7204 {
7205 char *d = xmprintf("%s.{right}", Ttab);
7206 char *c = xmprintf("cd %s/current/bin", scRoot);
7207
7208 I("ROBUST is starting up.");
7209 sendTmuxCmd(d, c);
7210 free(c);
7211 c = xmprintf("mono Robust.exe -inidirectory=%s/config/ROBUST", scRoot);
7212 sendTmuxCmd(d, c);
7213 free(c);
7214 waitTmuxText(d, "INITIALIZATION COMPLETE FOR ROBUST");
7215 I("ROBUST is done starting up.");
7216 la = waitLoadAverage(la, loadAverageInc / 3.0, simTimeOut / 3);
7217 free(d);
7218 }
7219
7220 for (i = 0; i < sims->num; i++)
7221 {
7222 char *sim = sims->sims[i], *name = getSimName(sims->sims[i]);
7223
7224 if (!checkSimIsRunning(sim))
7225 {
7226 char *nm = cleanSimName(name);
7227
7228 I("%s is starting up.", name);
7229 memset(toybuf, 0, sizeof(toybuf));
7230 snprintf(toybuf, sizeof(toybuf), "%s %s/%s new-window -dn '%s' -t '%s:%d' 'cd %s/current/bin; mono OpenSim.exe -inidirectory=%s/config/%s'",
7231 Tcmd, scRun, Tsocket, nm, Tconsole, i + 1, scRoot, scRoot, sim);
7232 int r = system(toybuf);
7233 if (!WIFEXITED(r))
7234 E("tmux new-window command failed!");
7235 else
7236 {
7237 memset(toybuf, 0, sizeof(toybuf));
7238 snprintf(toybuf, sizeof(toybuf), "INITIALIZATION COMPLETE FOR %s", name);
7239 waitTmuxText(nm, toybuf);
7240 I("%s is done starting up.", name);
7241 la = waitLoadAverage(la, loadAverageInc, simTimeOut);
7242 }
7243 free(nm);
7244 }
7245 free(name);
7246 }
7247
7248 }
7249 else if (!strcmp(cmd, "create")) // "create name x,y size"
7250 {
7251 }
7252 else if (!strcmp(cmd, "start")) // "start sim01" "start Welcome" "start" start everything
7253 {
7254// TODO - check if sim is down, but tmux window is still up, and close the tmux window first.
7255 }
7256 else if (!strcmp(cmd, "backup")) // "backup onefang rejected" "backup sim01" "backup Welcome" "backup" backup everything
7257 { // If it's not a sim code, and not a sim name, it's an account inventory.
7258 }
7259 else if (!strcmp(cmd, "gitAR")) // "gitAR i name"
7260 {
7261 }
7262 else if (!strcmp(cmd, "stop")) // "stop sim01" "stop Welcome" "stop" stop everything
7263 {
7264 }
7265
7266 freeSimList(sims);
7267
7268/*
7269 double sum;
7270
7271 // Load the file containing the script we are going to run
7272 status = luaL_loadfile(L, "script.lua");
7273 if (status)
7274 {
7275 // If something went wrong, error message is at the top of the stack
7276 E("Couldn't load file: %s", lua_tostring(L, -1));
7277 goto finished;
7278 }
7279*/
7280
7281 /*
7282 * Ok, now here we go: We pass data to the lua script on the stack.
7283 * That is, we first have to prepare Lua's virtual stack the way we
7284 * want the script to receive it, then ask Lua to run it.
7285 */
7286// lua_newtable(L); /* We will pass a table */
7287
7288 /*
7289 * To put values into the table, we first push the index, then the
7290 * value, and then call lua_rawset() with the index of the table in the
7291 * stack. Let's see why it's -3: In Lua, the value -1 always refers to
7292 * the top of the stack. When you create the table with lua_newtable(),
7293 * the table gets pushed into the top of the stack. When you push the
7294 * index and then the cell value, the stack looks like:
7295 *
7296 * <- [stack bottom] -- table, index, value [top]
7297 *
7298 * So the -1 will refer to the cell value, thus -3 is used to refer to
7299 * the table itself. Note that lua_rawset() pops the two last elements
7300 * of the stack, so that after it has been called, the table is at the
7301 * top of the stack.
7302 */
7303/*
7304 for (i = 1; i <= 5; i++)
7305 {
7306 lua_pushnumber(L, i); // Push the table index
7307 lua_pushnumber(L, i*2); // Push the cell value
7308 lua_rawset(L, -3); // Stores the pair in the table
7309 }
7310
7311 // By what name is the script going to reference our table?
7312 lua_setglobal(L, "foo");
7313
7314 // Ask Lua to run our little script
7315 result = lua_pcall(L, 0, LUA_MULTRET, 0);
7316 if (result)
7317 {
7318 E("Failed to run script: %s", lua_tostring(L, -1));
7319 goto finished;
7320 }
7321
7322 // Get the returned value at the top of the stack (index -1)
7323 sum = lua_tonumber(L, -1);
7324
7325 I("Script returned: %.0f", sum);
7326
7327 lua_pop(L, 1); // Take the returned value out of the stack
7328*/
7329
7330finished:
7331
7332 // An example of calling a toy directly.
7333// printf("\n\n");
7334// char *argv[] = {"ls", "-l", "-a", NULL};
7335// printf("%d\n", runToy(argv));
7336
7337
7338 puts("This is the end my friend.");
7339 fflush(stdout);
7340
7341 cleanup();
7342}