diff options
Diffstat (limited to 'src/sledjchisl/sledjchisl.c')
-rw-r--r-- | src/sledjchisl/sledjchisl.c | 7342 |
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 | |||
6 | USE_SLEDJCHISL(NEWTOY(sledjchisl, "m(mode):", TOYFLAG_USR|TOYFLAG_BIN)) | ||
7 | |||
8 | config 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 | ||
35 | extern 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 | |||
75 | GLOBALS( | ||
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 | */ | ||
115 | char *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 | */ | ||
178 | size_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. | ||
239 | int 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. | ||
273 | typedef 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> | ||
288 | int getrandom(void *b, size_t l, unsigned int f) | ||
289 | { | ||
290 | return (int) syscall(SYS_getrandom, b, l, f); | ||
291 | } | ||
292 | |||
293 | |||
294 | |||
295 | typedef struct _gridStats gridStats; | ||
296 | struct _gridStats | ||
297 | { | ||
298 | float next; | ||
299 | struct timeval last; | ||
300 | qhashtbl_t *stats; | ||
301 | }; | ||
302 | |||
303 | |||
304 | typedef struct _HTMLfile HTMLfile; | ||
305 | struct _HTMLfile | ||
306 | { | ||
307 | struct timespec last; | ||
308 | qlist_t *fragments; | ||
309 | }; | ||
310 | qhashtbl_t *HTMLfileCache = NULL; | ||
311 | |||
312 | |||
313 | typedef struct _reqData reqData; | ||
314 | |||
315 | typedef void *(*pageFunction) (char *file, reqData *Rd, HTMLfile *thisFile); | ||
316 | typedef struct _dynPage dynPage; | ||
317 | struct _dynPage | ||
318 | { | ||
319 | char *name; | ||
320 | pageFunction func; | ||
321 | }; | ||
322 | qhashtbl_t *dynPages; | ||
323 | static 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. | ||
336 | enum 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 | |||
359 | typedef struct _sesh sesh; | ||
360 | struct _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. | ||
372 | struct _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 | |||
385 | static 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, " name = %s\n", shs->name); | ||
394 | if (NULL != shs->UUID) | ||
395 | reply->addstrf(reply, " UUID = %s\n", shs->UUID); | ||
396 | reply->addstrf(reply, " salt = %s\n", shs->salt); | ||
397 | reply->addstrf(reply, " seshID = %s\n", shs->seshID); | ||
398 | reply->addstrf(reply, " timeStamp = %ld.%ld\n", shs->timeStamp[1].tv_sec, shs->timeStamp[1].tv_nsec); | ||
399 | reply->addstrf(reply, " sesh = %s\n", shs->sesh); | ||
400 | reply->addstrf(reply, " munchie = %s\n", shs->munchie); | ||
401 | reply->addstrf(reply, " toke_n_munchie = %s\n", shs->toke_n_munchie); | ||
402 | reply->addstrf(reply, " hashish = %s\n", shs->hashish); | ||
403 | reply->addstrf(reply, " leaf = %s\n", shs->leaf); | ||
404 | reply->addstrf(reply, " level = %d\n", (int) shs->level); | ||
405 | reply->addstr(reply, "</pre>\n"); | ||
406 | } | ||
407 | |||
408 | |||
409 | char toybuf[4096]; | ||
410 | lua_State *L; | ||
411 | qhashtbl_t *configs; | ||
412 | MYSQL *database, *dbconn; | ||
413 | unsigned int dbTimeout; | ||
414 | struct timespec dbLast; | ||
415 | my_bool dbReconnect; | ||
416 | gridStats *stats; | ||
417 | boolean isTmux = 0; | ||
418 | boolean isWeb = 0; | ||
419 | char *pwd = ""; | ||
420 | char *scRoot = "/opt/opensim_SC"; | ||
421 | char *scUser = "opensimsc"; | ||
422 | char *scBin = ""; | ||
423 | char *scEtc = ""; | ||
424 | char *scLib = ""; | ||
425 | char *scRun = ""; | ||
426 | char *scBackup = ""; | ||
427 | char *scCache = ""; | ||
428 | char *scData = ""; | ||
429 | char *scLog = ""; | ||
430 | char *Tconsole = "SledjChisl"; | ||
431 | char *Tsocket = "opensim-tmux.socket"; | ||
432 | char *Ttab = "SC"; | ||
433 | char *Tcmd = "tmux -S"; | ||
434 | char *webRoot = "/var/www/html"; | ||
435 | char *URL = "fcgi-bin/sledjchisl.fcgi"; | ||
436 | char *ToS = "Be good."; | ||
437 | char *webIframers = ""; | ||
438 | int seshRenew = 10 * 60; | ||
439 | int idleTimeOut = 30 * 60; | ||
440 | int seshTimeOut = 24 * 60 * 60; | ||
441 | int newbieTimeOut = 30; | ||
442 | float loadAverageInc = 0.5; | ||
443 | int simTimeOut = 45; | ||
444 | boolean DEBUG = TRUE; | ||
445 | qhashtbl_t *mimeTypes; | ||
446 | qlist_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 | ||
458 | char *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 | ||
472 | void 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 | |||
510 | static void addStrL(qlist_t *list, char *s) | ||
511 | { | ||
512 | list->addlast(list, s, strlen(s) + 1); | ||
513 | } | ||
514 | |||
515 | static 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 | |||
526 | char *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 | |||
543 | char *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. | ||
557 | void 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 | |||
575 | int 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 | |||
587 | int 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 | |||
599 | void 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 | |||
622 | float 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. | ||
648 | struct dirtree *dirtree_handle_callback(struct dirtree *new, int (*callback)(struct dirtree *node)); | ||
649 | |||
650 | typedef struct _simList simList; | ||
651 | struct _simList | ||
652 | { | ||
653 | int len, num; | ||
654 | char **sims; | ||
655 | }; | ||
656 | |||
657 | static 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 \ " ` | ||
676 | char *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 | |||
694 | simList *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 | |||
708 | void 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 | |||
718 | static 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 | |||
730 | char *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". | ||
780 | int 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 | |||
842 | static 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 | |||
850 | static void printEnv(char **envp) | ||
851 | { | ||
852 | for ( ; *envp != NULL; envp++) | ||
853 | D("%s", *envp); | ||
854 | } | ||
855 | |||
856 | |||
857 | typedef struct _rowData rowData; | ||
858 | struct _rowData | ||
859 | { | ||
860 | char **fieldNames; | ||
861 | qlist_t *rows; | ||
862 | }; | ||
863 | |||
864 | static 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 | |||
875 | static 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 | |||
888 | typedef struct _dbFields dbFields; | ||
889 | struct _dbFields | ||
890 | { | ||
891 | qlisttbl_t *flds; | ||
892 | int count; | ||
893 | }; | ||
894 | typedef struct _dbField dbField; | ||
895 | struct _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 | |||
904 | void 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 | |||
930 | enum dbCommandType | ||
931 | { | ||
932 | CT_SELECT, | ||
933 | CT_CREATE, | ||
934 | CT_UPDATE, | ||
935 | CT_NONE | ||
936 | }; | ||
937 | |||
938 | typedef struct _dbRequest dbRequest; | ||
939 | struct _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 | |||
953 | void 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 | |||
1021 | void 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 | |||
1054 | static 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. | ||
1123 | static 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" | ||
1138 | static 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 | |||
1153 | dbFields *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 | |||
1165 | d("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. | ||
1221 | http://karlssonondatabases.blogspot.com/2010/07/prepared-statements-are-they-useful-or.html | ||
1222 | https://blog.cotten.io/a-taste-of-mysql-in-c-87c5de84a31d?gi=ab3dd1425b29 | ||
1223 | https://raspberry-projects.com/pi/programming-in-c/databases-programming-in-c/mysql/accessing-the-database | ||
1224 | |||
1225 | IG and CG now both have sims connected to other grids, so some sort of | ||
1226 | multi database solution would be good, then we can run the grid and the | ||
1227 | external sims all in one. | ||
1228 | |||
1229 | Not sure if this'll work with Count(*). | ||
1230 | |||
1231 | --------------------------------------------- | ||
1232 | |||
1233 | The complicated bit is the binds. | ||
1234 | |||
1235 | You are binding field values to C memory locations. | ||
1236 | The parameters and returned fields need binds. | ||
1237 | Mostly seems to be the value parts of the SQL statements. | ||
1238 | |||
1239 | I 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 | |||
1248 | int 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 | |||
1264 | t("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 | |||
1371 | d("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 | { | ||
1529 | I("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 | |||
1977 | freeIt: | ||
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 | |||
1986 | end: | ||
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. | ||
1998 | void 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); | ||
2011 | d("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 | |||
2023 | my_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 | |||
2073 | my_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 | |||
2123 | void replaceStr(qhashtbl_t *ssi, char *key, char *value) | ||
2124 | { | ||
2125 | ssi->putstr(ssi, key, value); | ||
2126 | } | ||
2127 | |||
2128 | void 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 | |||
2137 | float 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 | |||
2162 | gridStats *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 | |||
2277 | qhashtbl_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); | ||
2297 | d(" %s = %s", qstrtrim_head(key), val); | ||
2298 | val = NULL; | ||
2299 | } | ||
2300 | } | ||
2301 | if (NULL != val) | ||
2302 | { | ||
2303 | ret->putstr(ret, qstrtrim_head(key), val); | ||
2304 | d(" %s = %s", qstrtrim_head(key), val); | ||
2305 | } | ||
2306 | free(txt); | ||
2307 | return ret; | ||
2308 | } | ||
2309 | |||
2310 | void 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 | |||
2326 | void 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, " %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 | ||
2339 | enum cookieSame | ||
2340 | { | ||
2341 | CS_NOT, | ||
2342 | CS_STRICT, | ||
2343 | CS_LAX, // Apparently the default set by browsers these days. | ||
2344 | CS_NONE | ||
2345 | }; | ||
2346 | typedef struct _cookie cookie; | ||
2347 | struct _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 | |||
2356 | void 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 | |||
2368 | cookie *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 | |||
2400 | char *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 | |||
2410 | void 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, " %s = %s\n", obj.name, ((cookie *) obj.data)->value); | ||
2418 | tbl->unlock(tbl); | ||
2419 | reply->addstr(reply, "</pre>\n"); | ||
2420 | } | ||
2421 | |||
2422 | void 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 | |||
2442 | qlist_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 | |||
2462 | enum fragmentType | ||
2463 | { | ||
2464 | FT_TEXT, | ||
2465 | FT_PARAM, | ||
2466 | FT_LUA | ||
2467 | }; | ||
2468 | |||
2469 | typedef struct _fragment fragment; | ||
2470 | struct _fragment | ||
2471 | { | ||
2472 | enum fragmentType type; | ||
2473 | int length; | ||
2474 | char *text; | ||
2475 | }; | ||
2476 | |||
2477 | static 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 | |||
2492 | static 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? | ||
2531 | char *HTMLentities[] = | ||
2532 | { | ||
2533 | "", "", "", "", "", "", "", "", "", // NUL SOH STX ETX EOT ENQ ACK BEL BS | ||
2534 | "	", "
", | ||
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 | "!", """, | ||
2538 | "#", "$", | ||
2539 | "%", "&", | ||
2540 | "'", | ||
2541 | "(", ")", | ||
2542 | "*", | ||
2543 | "+", | ||
2544 | ",", | ||
2545 | "-", | ||
2546 | ".", | ||
2547 | "/", | ||
2548 | "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", | ||
2549 | ":", ";", | ||
2550 | "<", "=", ">", | ||
2551 | "?", | ||
2552 | "@", | ||
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 | "[", "\", "]", | ||
2555 | "^", | ||
2556 | "_", | ||
2557 | "`", | ||
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 | "{", "|", "}", | ||
2560 | "~", | ||
2561 | "", // DEL | ||
2562 | " " // This is actually 160, not 128, but I hack around that. | ||
2563 | }; | ||
2564 | static 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 . | ||
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 | |||
2613 | static 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 | |||
2677 | static 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 | |||
2687 | static 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 | } | ||
2693 | static void HTMLformEnd(qgrow_t *reply) | ||
2694 | { | ||
2695 | reply->addstrf(reply, " </form>\n"); | ||
2696 | } | ||
2697 | |||
2698 | static 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 | |||
2713 | static void HTMLtextArea(qgrow_t *reply, char *name, char *title, int rows, int cols, int min, int max, char *holder, char *comp, char *spell, char *wrap, char *val, boolean required, boolean readOnly) | ||
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 | |||
2746 | static 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 | |||
2764 | static 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 | } | ||
2771 | static void HTMLselectEnd(qgrow_t *reply) | ||
2772 | { | ||
2773 | reply->addstr(reply, " </select></label></p>\n \n"); | ||
2774 | } | ||
2775 | static void HTMLselectEndNo(qgrow_t *reply) | ||
2776 | { | ||
2777 | reply->addstr(reply, " </select></p>"); | ||
2778 | } | ||
2779 | |||
2780 | static 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 | |||
2789 | static 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 | |||
2794 | static 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 | |||
2811 | static int count = 0; | ||
2812 | void 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 | |||
2866 | static 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. " | ||
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. " | ||
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. Now you have to wait for an admin to approve your new account. " | ||
2879 | " They should check with the person you listed as vouching for you first. 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 | |||
2894 | fragment *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 | |||
2906 | qlist_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 | |||
2967 | void 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 | |||
2990 | HTMLfile *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 | |||
3167 | https://owasp.org/www-project-cheat-sheets/cheatsheets/Input_Validation_Cheat_Sheet.html#Email_Address_Validation | ||
3168 | https://cheatsheetseries.owasp.org/ | ||
3169 | https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html | ||
3170 | https://owasp.org/www-project-cheat-sheets/cheatsheets/Authentication_Cheat_Sheet.html | ||
3171 | https://softwareengineering.stackexchange.com/questions/46716/what-technical-details-should-a-programmer-of-a-web-application-consider-before | ||
3172 | https://wiki.owasp.org/index.php/OWASP_Guide_Project | ||
3173 | https://stackoverflow.com/questions/549/the-definitive-guide-to-form-based-website-authentication?rq=1 | ||
3174 | https://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 | |||
3184 | Large 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. | ||
3187 | Cryptographically 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. | ||
3191 | A 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. | ||
3198 | Double 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. | ||
3207 | TODO - think it through, is it really secure against session hijacking? | ||
3208 | TODO - document why we redirect POST to GET, coz it's a pain in the arse, and we have to do things twice. | ||
3209 | |||
3210 | SOOOOO - 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 | |||
3241 | I should make it easy to change the HMAC() function. Less important for these short lived sessions, more important for the linky URLs, most important for stored password hashes. | ||
3242 | Same for the pepper. | ||
3243 | |||
3244 | The 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 | |||
3247 | NOTE - storing a pepper on the same RAID array as everything else will be a problem when it comes time to replace one of the disks. | ||
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 | |||
3251 | https://stackoverflow.com/questions/16891729/best-practices-salting-peppering-passwords | ||
3252 | */ | ||
3253 | |||
3254 | |||
3255 | qlisttbl_t *accountLevels = NULL; | ||
3256 | |||
3257 | |||
3258 | static 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." | ||
3265 | https://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. | ||
3268 | I 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 | */ | ||
3271 | static 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. | ||
3281 | char *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 | |||
3298 | char *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 | |||
3305 | T("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 | |||
3344 | int 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)); | ||
3389 | d("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 | |||
3412 | char *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. " | ||
3422 | "It will be from %s@%s, and it might be in your spam folder, coz these sorts of emails sometimes end up there. " | ||
3423 | "You should add that email address to your contacts, or otherwise let it through your spam filter. " | ||
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 | |||
3436 | static void freeSesh(reqData *Rd, boolean linky, boolean wipe) | ||
3437 | { | ||
3438 | char *file = NULL; | ||
3439 | sesh *shs = &Rd->shs; | ||
3440 | |||
3441 | T("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 | |||
3508 | static 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. | ||
3553 | t("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 | { | ||
3562 | t("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 | { | ||
3572 | t("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 | { | ||
3583 | t("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 | { | ||
3594 | t("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 | |||
3684 | static 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 | |||
3717 | char *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 | |||
3737 | typedef struct _systemFolders systemFolders; | ||
3738 | struct _systemFolders | ||
3739 | { | ||
3740 | char *name; | ||
3741 | short type; | ||
3742 | }; | ||
3743 | |||
3744 | systemFolders 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 | |||
3772 | boolean 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 | |||
3788 | boolean 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 | |||
3804 | boolean 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 | |||
3849 | static 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 | } | ||
3912 | notWritten: | ||
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); | ||
4109 | T(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 | |||
4138 | static 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 | |||
4146 | T("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) | ||
4233 | CRAP (Create, Replicate, Append, Process) | ||
4234 | Though I prefer - | ||
4235 | DAVE (Delete, Add, View, Edit), coz the names are shorter. B-) | ||
4236 | On the other hand, list or browse needs to be added, which is why they have | ||
4237 | BREAD (Browse, Read, Edit, Add, Delete) | ||
4238 | CRUDL (Create, Read, Update, Delete, List) | ||
4239 | CRUDE (Create, Read, Update, Delete, Experience) | ||
4240 | Maybe - | ||
4241 | DAVEE (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 | |||
4258 | typedef struct _inputField inputField; | ||
4259 | typedef struct _inputSub inputSub; | ||
4260 | typedef struct _inputForm inputForm; | ||
4261 | typedef struct _inputValue inputValue; | ||
4262 | |||
4263 | typedef int (*inputFieldValidFunc) (reqData *Rd, inputForm *iF, inputValue *iV); | ||
4264 | typedef void (*inputFieldShowFunc) (reqData *Rd, inputForm *iF, inputValue *iV); | ||
4265 | typedef int (*inputSubmitFunc) (reqData *Rd, inputForm *iF, inputValue *iV); | ||
4266 | typedef void (*inputFormShowFunc) (reqData *Rd, inputForm *iF, inputValue *iV); | ||
4267 | |||
4268 | struct _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 | }; | ||
4279 | struct _inputSub | ||
4280 | { | ||
4281 | char *name, *title, *help, *outputForm; | ||
4282 | inputSubmitFunc submit; | ||
4283 | }; | ||
4284 | struct _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 | }; | ||
4293 | struct _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 | |||
4302 | static 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 | { | ||
4440 | W("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 | { | ||
4450 | W("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 | { | ||
4514 | t("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 | |||
4534 | static void sessionWeb(reqData *Rd, inputForm *iF, inputValue *iV) | ||
4535 | { | ||
4536 | HTMLhidden(Rd->reply, iV->field->name, iV->value); | ||
4537 | } | ||
4538 | |||
4539 | /* | ||
4540 | static 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 | |||
4557 | static void UUIDWeb(reqData *Rd, inputForm *iF, inputValue *iV) | ||
4558 | { | ||
4559 | HTMLhidden(Rd->reply, iV->field->name, iV->value); | ||
4560 | } | ||
4561 | */ | ||
4562 | |||
4563 | static 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 | |||
4658 | static 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 | |||
4667 | static 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 | |||
4696 | static 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. " | ||
4700 | "I highly recommend using a password manager. KeePass and it's variations is a great password manager.</p>\n"); | ||
4701 | } | ||
4702 | |||
4703 | static 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 | } | ||
4757 | static 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. " | ||
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 | |||
4766 | char *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 | }; | ||
4781 | static 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 | } | ||
4846 | static 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 | } | ||
4871 | static 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 | } | ||
4891 | static void DoBWeb(reqData *Rd, inputForm *oF, inputValue *oV) | ||
4892 | { | ||
4893 | } | ||
4894 | |||
4895 | static 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 | } | ||
4924 | static 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 | } | ||
4928 | static 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 | } | ||
4932 | static void legalWeb(reqData *Rd, inputForm *oF, inputValue *oV) | ||
4933 | { | ||
4934 | } | ||
4935 | static 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 | |||
4940 | static 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 | } | ||
4956 | static 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 | |||
4961 | static 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 | |||
4978 | static 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 | |||
4987 | static 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 | |||
5010 | static 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 | |||
5024 | static 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 | |||
5041 | static 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 | |||
5049 | static 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 | |||
5057 | static 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. | ||
5071 | static 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 | |||
5094 | static 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 | |||
5155 | static 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 | |||
5220 | d("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 | } | ||
5236 | d("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 | { | ||
5270 | T("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; | ||
5299 | T("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 | |||
5359 | static 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() | ||
5380 | static 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() | ||
5420 | static 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 | |||
5463 | static 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. | ||
5512 | static 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 | |||
5549 | static 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 | |||
5556 | d("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 | } | ||
5592 | static 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 | |||
5598 | d("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 | |||
5612 | static 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 | |||
5619 | static 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 | */ | ||
5646 | typedef struct _RdAndListTbl RdAndListTbl; | ||
5647 | struct _RdAndListTbl | ||
5648 | { | ||
5649 | reqData *Rd; | ||
5650 | qlisttbl_t *list; | ||
5651 | }; | ||
5652 | static 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 | |||
5666 | t("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 | } | ||
5675 | qlisttbl_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 | } | ||
5689 | static 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 | } | ||
5732 | static int accountExploreValidatedVoucherSub(reqData *Rd, inputForm *iF, inputValue *iV) | ||
5733 | { | ||
5734 | int ret = 0; | ||
5735 | return ret; | ||
5736 | } | ||
5737 | |||
5738 | |||
5739 | qhashtbl_t *accountPages = NULL; | ||
5740 | inputForm *newInputForm(char *name, char *title, char *help, inputFormShowFunc web, inputFormShowFunc eWeb) | ||
5741 | { | ||
5742 | inputForm *ret = xmalloc(sizeof(inputForm)); | ||
5743 | |||
5744 | d("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 | |||
5754 | inputField *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 | |||
5767 | void inputFieldExtra(inputField *ret, signed char flags, short viewLength, short maxLength) | ||
5768 | { | ||
5769 | ret->flags = flags; | ||
5770 | ret->viewLength = viewLength; ret->maxLength = maxLength; | ||
5771 | } | ||
5772 | |||
5773 | void 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 | |||
5789 | void 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 | |||
5805 | void 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 | |||
5818 | void 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 | |||
5831 | inputSub *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 | |||
5854 | URL query Values actually provided by the user in the FORM, and other things. | ||
5855 | POST body Values actually provided by the user in the FORM. | ||
5856 | cookies | ||
5857 | https://stackoverflow.com/questions/4056306/how-to-handle-multiple-cookies-with-the-same-name | ||
5858 | |||
5859 | headers includes HTTP_COOKIE and QUERY_STRING | ||
5860 | env includes headers and HTTP_COOKIE and QUERY_STRING | ||
5861 | |||
5862 | database 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 | */ | ||
5867 | char *sourceTypes[] = | ||
5868 | { | ||
5869 | "cookies", | ||
5870 | "body", | ||
5871 | "queries", | ||
5872 | "stuff" | ||
5873 | }; | ||
5874 | |||
5875 | static 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 | |||
5939 | void 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 | |||
5966 | void 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 | |||
6372 | static 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 | |||
6441 | void 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 - | ||
6513 | To change or extend the list of standard libraries to load, copy | ||
6514 | src/lib_init.c to your project and modify it accordingly. Make sure the | ||
6515 | jit 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 "-" -> "_". | ||
6847 | t("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); | ||
6859 | if ((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 | |||
6900 | t("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. | ||
6905 | t("QUERY"); | ||
6906 | Rd->queries = toknize(Rd->headers->getstr(Rd->headers, "QUERY_STRING", false), "=&"); | ||
6907 | santize(Rd->queries); | ||
6908 | } | ||
6909 | else | ||
6910 | { | ||
6911 | T("ignoring QUERY"); | ||
6912 | Rd->queries = qhashtbl(0, 0); | ||
6913 | free(Rd->RUri); | ||
6914 | Rd->RUri = xmprintf("%s%s", Rd->Script, Rd->Path); | ||
6915 | } | ||
6916 | t("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); | ||
7019 | goto 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 | |||
7062 | sendReply: | ||
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 | |||
7124 | fcgiDone: | ||
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 | |||
7330 | finished: | ||
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 | } | ||