From bd58d3012c26d16150f650c389d1136741d3939d Mon Sep 17 00:00:00 2001
From: onefang
Date: Tue, 8 Sep 2020 21:34:54 +1000
Subject: Add the SledjChisl stuff.
---
example/etc/apache2/sledjchisl.fcgi.conf | 37 +
example/www/SledjChisl.css | 63 +
example/www/SledjHamr.png | Bin 0 -> 1655233 bytes
example/www/SledjHamrIcon.png | Bin 0 -> 95362 bytes
example/www/SledjHamrIconSmall.png | Bin 0 -> 8535 bytes
example/www/about.html | 11 +
example/www/debugStyle.css | 36 +
example/www/help.html | 11 +
example/www/loginpage.html | 109 +
example/www/password_help.html | 11 +
example/www/register.html | 15 +
example/www/stats.html | 41 +
src/.sledjChisl.conf.lua | 58 +
src/BuildIt.sh | 132 +
src/boxes/BOXES.txt | 1130 +++++
src/boxes/BUGS.txt | 6 +
src/boxes/README | 34 +
src/boxes/boxes.c | 2517 ++++++++++
src/boxes/dumbsh.c | 283 ++
src/boxes/handlekeys.c | 445 ++
src/boxes/handlekeys.h | 76 +
src/boxes/showkey.c | 149 +
src/build/fcgi2 | 1 +
src/build/luajit | 1 +
src/build/qlibc | 1 +
src/build/toybox | 1 +
src/git-sub-modules/README | 3 +
src/git-sub-modules/fcgi2 | 1 +
src/git-sub-modules/luajit | 1 +
src/git-sub-modules/qlibc | 1 +
src/git-sub-modules/toybox | 1 +
src/miniconfig | 13 +
src/sledjchisl/README | 118 +
src/sledjchisl/fcgi_SC.c | 13 +
src/sledjchisl/fcgi_SC.h | 136 +
src/sledjchisl/script.lua | 18 +
src/sledjchisl/sledjchisl.c | 7342 ++++++++++++++++++++++++++++++
src/tickle.lua | 26 +
38 files changed, 12841 insertions(+)
create mode 100644 example/etc/apache2/sledjchisl.fcgi.conf
create mode 100644 example/www/SledjChisl.css
create mode 100644 example/www/SledjHamr.png
create mode 100644 example/www/SledjHamrIcon.png
create mode 100644 example/www/SledjHamrIconSmall.png
create mode 100644 example/www/about.html
create mode 100644 example/www/debugStyle.css
create mode 100644 example/www/help.html
create mode 100644 example/www/loginpage.html
create mode 100644 example/www/password_help.html
create mode 100644 example/www/register.html
create mode 100644 example/www/stats.html
create mode 100644 src/.sledjChisl.conf.lua
create mode 100755 src/BuildIt.sh
create mode 100644 src/boxes/BOXES.txt
create mode 100644 src/boxes/BUGS.txt
create mode 100644 src/boxes/README
create mode 100644 src/boxes/boxes.c
create mode 100644 src/boxes/dumbsh.c
create mode 100644 src/boxes/handlekeys.c
create mode 100644 src/boxes/handlekeys.h
create mode 100644 src/boxes/showkey.c
create mode 160000 src/build/fcgi2
create mode 160000 src/build/luajit
create mode 160000 src/build/qlibc
create mode 160000 src/build/toybox
create mode 100644 src/git-sub-modules/README
create mode 160000 src/git-sub-modules/fcgi2
create mode 160000 src/git-sub-modules/luajit
create mode 160000 src/git-sub-modules/qlibc
create mode 160000 src/git-sub-modules/toybox
create mode 100644 src/miniconfig
create mode 100644 src/sledjchisl/README
create mode 100644 src/sledjchisl/fcgi_SC.c
create mode 100644 src/sledjchisl/fcgi_SC.h
create mode 100644 src/sledjchisl/script.lua
create mode 100644 src/sledjchisl/sledjchisl.c
create mode 100755 src/tickle.lua
diff --git a/example/etc/apache2/sledjchisl.fcgi.conf b/example/etc/apache2/sledjchisl.fcgi.conf
new file mode 100644
index 0000000..95458cd
--- /dev/null
+++ b/example/etc/apache2/sledjchisl.fcgi.conf
@@ -0,0 +1,37 @@
+# This is just an example to use to modify your real site configuration file.
+
+LoadModule proxy_fcgi_module /usr/lib/apache2/modules/mod_proxy_fcgi.so
+
+
+ + diff --git a/example/www/debugStyle.css b/example/www/debugStyle.css new file mode 100644 index 0000000..98645cc --- /dev/null +++ b/example/www/debugStyle.css @@ -0,0 +1,36 @@ +.hoverWrapper0:hover #hoverShow0 +{ + display: block; + border-style: solid; + border-color: fuchsia; +} +.hoverWrapper0 #hoverShow0 +{ + display: none; + background-color: #222222; + text-align: left; + position: absolute; + width: 100%; + border-style: solid; + border-color: fuchsia; +} +.hoverWrapper1:hover #hoverShow1 +{ + display: block; + border-style: solid; + border-color: fuchsia; +} +.hoverWrapper1 #hoverShow1 +{ + display: none; + background-color: #222222; + text-align: left; + position: absolute; + width: 100%; + border-style: solid; + border-color: fuchsia; +} +.hoverItem +{ + border: 1px solid fuchsia; +} diff --git a/example/www/help.html b/example/www/help.html new file mode 100644 index 0000000..9a75aa6 --- /dev/null +++ b/example/www/help.html @@ -0,0 +1,11 @@ + +
+ + diff --git a/example/www/loginpage.html b/example/www/loginpage.html new file mode 100644 index 0000000..7a221dc --- /dev/null +++ b/example/www/loginpage.html @@ -0,0 +1,109 @@ + +
+
DEBUG
++
+
+
Perhaps describe here.
++
There are members of this grid.
+There are locals and hypergrid visitors in world.
+There are locals out on the hypergrid.
+There have been locals and visitors on this grid in the last month.
+There are regions, though some might not be online right now.
+Maybe add some news or events here, or something.
+CSS by Taylor Temper, photo by onefang rejected.
+is running , + part of the SledjHamr project.
++ + diff --git a/example/www/register.html b/example/www/register.html new file mode 100644 index 0000000..46317e6 --- /dev/null +++ b/example/www/register.html @@ -0,0 +1,15 @@ + +
+
If you want to register an account on this grid, ask the person that runs it to do that for you.
++
If you want to register an account on this grid, click here (when it's written).
++ + diff --git a/example/www/stats.html b/example/www/stats.html new file mode 100644 index 0000000..7b51b06 --- /dev/null +++ b/example/www/stats.html @@ -0,0 +1,41 @@ + +
Grid is -
+Login URI -
+Login page - /loginpage.html">/loginpage.html
+There are members of this grid.
+There are locals and hypergrid visitors in world right now.
+There are locals out on the hypergrid right now.
+There have been locals and hypergrid visitors on this grid in the last 30 days ( total).
+There have been locals and hypergrid visitors on this grid in the last 60 days ( total).
+There are regions, though might not be online right now.
+There may be regions online, with a total area of roughly square metres.
+There are varregions that might be online now.
+There are normal regions that might be online now.
+is running , + part of the SledjHamr project.
+This statistics page will update every two minutes.
+ + + diff --git a/src/.sledjChisl.conf.lua b/src/.sledjChisl.conf.lua new file mode 100644 index 0000000..6b8f29b --- /dev/null +++ b/src/.sledjChisl.conf.lua @@ -0,0 +1,58 @@ +-- sledjChislConfig.lua + +-- This works coz LuaJIT automatically loads the jit module. +if type(jit) == 'table' then + io.write('.sledjChisl.conf.lua is being run by ' .. jit.version .. ' under ' .. jit.os .. ' on a ' .. jit.arch .. '\n') +else + io.write('.sledjChisl.conf.lua is being run by Lua version ' .. _VERSION .. '\n') +end + +config = +{ + ["performance"] = "default"; -- fast, balanced, default, lean + ["debug"] = true; + ["scRoot"] = "/opt/opensim_SC"; + ["scUser"] = "opensimsc"; + ["Tconsole"] = "SledjChisl"; + ["Tsocket"] = "opensim-tmux.socket"; + ["Ttab"] = "SC"; + ["loadAverageInc"] = 0.7; + ["simTimeOut"] = 45; -- seconds + ["webRoot"] = "/var/www/html"; + ["webHost"] = "localhost"; + ["URL"] = "sledjchisl.fcgi"; + ["webIframers"] = ""; -- Space separated list of hosts allowed to iFrame us, coz someone asked. Include the "https://" bit. + ["seshRenew"] = 10 * 60; -- seconds + ["idleTimeOut"] = 30 * 60; -- seconds + ["seshTimeOut"] = 24 * 60 * 60; -- seconds + ["newbieTimeOut"] = 30; -- days + ["pepper"] = "My long beard is salt and pepper coloured, though there are no birds in it, only breakfast."; + ["ToS"] = [[ + +Don't do anything that is illegal anywhere in the world. + +Well, that wont work, almost everything is illegal somewhere in the +world. + +Don't do anything that is against the moral code of the system admins. + +Well, except that one thing, you know, that they'll put up with coz they +are nice people, but it's wrong m'kay. + +Don't be mean to anyone, except Dave, coz he smells evil. + +Well, it's not that Dave smells evil, he's just differently fragranced, +and our overwashed germophobe society is well trained to demonize those +that smell differently. So be extra nice to Dave, coz he's a great guy, +and is tired of everyone being mean to him just coz he's trying to be +good for the environment and his health. Which means he smells different +to whatever perfume is fashionable this year, coz the corporations want +to sell that this year. I blame marketing, they're actually evil. +Sorry, went off on a rant there. + +Oh just respect and be nice to everyone dammit, unless they ask nicely +otherwise. Also be good for the environment and stay healthy. + +]]; +} +return config diff --git a/src/BuildIt.sh b/src/BuildIt.sh new file mode 100755 index 0000000..6696210 --- /dev/null +++ b/src/BuildIt.sh @@ -0,0 +1,132 @@ +#!/bin/bash + +mkdir -p build + +# Poor mans git sub modules / subtrees, coz otherwise it gets complex. +if [ ! -d git-sub-modules/fcgi2 ]; then + pushd git-sub-modules >/dev/null + git clone https://github.com/FastCGI-Archives/fcgi2.git + popd >/dev/null +else + pushd git-sub-modules/fcgi2 >/dev/null + echo "Updating fcgi2." +# git pull | grep "Already up-to-date." && rm -fr build/fcgi2 + popd >/dev/null +fi + +if [ ! -d build/fcgi2 ]; then + cp -r git-sub-modules/fcgi2 build/ + pushd build/fcgi2 >/dev/null +# make distclean + ./autogen.sh + ./configure + sed -e "s/#define PACKAGE/#define FCGI_PACKAGE/g" -i fcgi_config.h + sed -e "s/#define VERSION /#define FCGI_VERSION /g" -i fcgi_config.h + make + popd >/dev/null +fi + +echo "" +echo "" + +if [ ! -d git-sub-modules/luajit ]; then + pushd git-sub-modules >/dev/null + git clone https://luajit.org/git/luajit-2.0.git + mv luajit-2.0 luajit + pushd luajit >/dev/null + git checkout v2.1 + popd >/dev/null + popd >/dev/null +else + pushd git-sub-modules/luajit >/dev/null + echo "Updating LuaJIT." +# git pull | grep "Already up-to-date." && rm -fr build/luajit + popd >/dev/null +fi + +if [ ! -d build/luajit ]; then + rm -fr build/luajit + cp -r git-sub-modules/luajit build/ + + pushd build/luajit >/dev/null + make clean + make amalg + popd >/dev/null +fi + +echo "" +echo "" + +if [ ! -d git-sub-modules/qlibc ]; then + pushd git-sub-modules >/dev/null + git clone https://github.com/wolkykim/qlibc.git + popd >/dev/null +else + pushd git-sub-modules/qlibc >/dev/null + echo "Updating qlibc." +# git pull | grep "Already up-to-date." && rm -fr build/qlibc + popd >/dev/null +fi + +if [ ! -d build/qlibc ]; then + rm -fr build/qlibc + cp -r git-sub-modules/qlibc build/ + + pushd build/qlibc >/dev/null + make clean + ./configure + make + popd >/dev/null +fi + +echo "" +echo "" + +if [ ! -d git-sub-modules/toybox ]; then + pushd git-sub-modules >/dev/null + git clone https://github.com/landley/toybox.git + popd >/dev/null +else + pushd git-sub-modules/toybox >/dev/null + echo "Updating toybox." +# git pull | grep "Already up-to-date." && rm -fr build/toybox + popd >/dev/null +fi + +if [ ! -d build/toybox ]; then + rm -fr build/toybox + cp -r git-sub-modules/toybox build/ + ln -fs ../../../boxes build/toybox/toys/boxes + ln -fs ../../../sledjchisl build/toybox/toys/sledjchisl + ln -fs ../toys/sledjchisl/fcgi_SC.c build/toybox/lib + ln -fs ../toys/sledjchisl/fcgi_SC.h build/toybox/lib + ln -fs ../toys/boxes/handlekeys.c build/toybox/lib + ln -fs ../toys/boxes/handlekeys.h build/toybox/lib + + pushd build/toybox >/dev/null + sed -e "s/strend(/tb_strend(/g" -i lib/lib.h + find ./ -type f -name "*.c" -exec sed -e "s/strend(/tb_strend(/g" -i {} \; + make clean + #make defconfig + #make menuconfig + make allnoconfig KCONFIG_ALLCONFIG=../../miniconfig || exit 1 + popd >/dev/null +fi + +echo "" +echo "" + +pushd build/toybox >/dev/null +export CFLAGS="-I../luajit/src -I../fcgi2 -I../fcgi2/include -I../qlibc/include/qlibc $(mysql_config --cflags) -g3" +export LDFLAGS="-L../luajit/src -L../fcgi2/libfcgi/.libs -L../qlibc/lib $(mysql_config --libs) -Wl,-E -l:libluajit.a -l:libqlibcext.a -l:libfcgi.a -l:libqlibc.a -lcrypto -luuid" +make || exit 1 +popd >/dev/null +ln -fs ../src/build/toybox/toybox ../bin/sledjchisl + +#sudo rm -f /opt/opensim_SC/var/cache/sledjchisl.socket +#sudo rm -f /opt/opensim_SC/var/cache/sessions/* +#sudo rm -f /opt/opensim_SC/var/lib/users/* +#sudo spawn-fcgi -n -u opensimsc -s /opt/opensim_SC/var/cache/sledjchisl.socket -M 0660 -G www-data -- /usr/bin/valgrind --leak-check=full build/toybox/generated/unstripped/toybox sledjchisl +##sudo spawn-fcgi -n -u opensimsc -s /opt/opensim_SC/var/cache/sledjchisl.socket -M 0660 -G www-data -- /usr/bin/valgrind --leak-check=full build/toybox/generated/unstripped/toybox sledjchisl 2>&1 | tee log.txt +##sudo spawn-fcgi -n -u opensimsc -s /opt/opensim_SC/var/cache/sledjchisl.socket -M 0660 -G www-data -- /usr/bin/valgrind --leak-check=full --show-leak-kinds=all build/toybox/generated/unstripped/toybox sledjchisl +###sudo spawn-fcgi -n -u opensimsc -s /opt/opensim_SC/var/cache/sledjchisl.socket -M 0660 -G www-data -- /usr/bin/ddd build/toybox/generated/unstripped/toybox sledjchisl diff --git a/src/boxes/BOXES.txt b/src/boxes/BOXES.txt new file mode 100644 index 0000000..745b7c9 --- /dev/null +++ b/src/boxes/BOXES.txt @@ -0,0 +1,1130 @@ +What's needed for MC like program. Call the library toyboxes, and the MC clone toysoldier. B-) + +Use ANSI for terminal control. +UTF-8 support is welcome in toybox where it makes sense, here it makes sense. + +Command defining - key, name, script, context. + Internal commands for the scripts. + Which can depend on the context. + Context is important. Gotta handle stuff like vi modes, MC browse / view / edit modes, less search popping into readline mode, etc. + Learnable keys ala GIMP. + +Split the screen up into boxes. + Each box is a context. + Current box should have it's box graphics drawn in a different colour to highlight it. + Tab/shift-Tab cycles through boxes. + Though the editor has it's own use for tab. Ctrl-tab as an alias perhaps? + Horizontal / vertical splits, with some sort of size control. + Each box can be split h or v once, + with a control of what proportion the current box has (initially half), + a minimum size set by the contents, + and initially a copy of the pointer to the function that supplies it's contents / deals with it's input / includes it's context. + Later it can have that pointer set to something else. + Any given box can be deleted, which deletes it's sub boxes, drops it's pointer, and merges it's area with the one it split from. + See if screen and tmux agree on keys to use for box control. + Though, as usual, it's definable, but screen/tmux can be the defaults. + Choose to make a box full screen. With menu to select other boxes, or swap back to full set of boxes. + Can be without borders and such. + + The borders can be the usual box graphics, +-| characters, or inverse spaces, in that order of priority. + Show bits of text on top and bottom borders (left and right sides of them). + MC includes a couple of tiny mouse controlled widgets. + Perhaps a scroll widget in left or right borders. Emacs dired has that. + + Single line 'boxes', across the entire terminal, or across each box. + Should be context sensitive. + Menu - a list of commands showing their names and keys, with sub menus. + Function keys - Any function keys with commands showing the command names and key. + Perhaps could just be a specialized menu. + Programmable status line. + + Contents scrolling. + Virtual memory buffers. + If a file viewer, just mmap it. + Editor should be able to only load in bits of a large file, perhaps with a UNDO/REDO buffer. + If command output, can create a temp file and mmap it. + + file viewer (less, man). + editor (vi, mcedit, nano). + Like e3, make generic editor, with pluggable command keys, that kick in depending on how it's called. + Uses a file list in a box to select files for opening. + Poor mans top and similar commands. + Top at least could make use of internal boxes, with the command list using a sortable list. + Put command output in boxes. Each line can be selected and operated on. + Title bar across top, with click to sort style column names. + Each line is a single line 'box'. + ls. + ls with display options. + archive listing + find command result. + All these can show current directory with diving in and out, or tree and current directory in a sub box. + Also, allow an edit mode for editing the file attributes that are displayed, inline and full box if possible. + Shell console, with various text substitutions on input. + Can be a single line 'box' as well as an ordinary box. + Though the ordinary ones show their output in their box, + but the single line one should swap to a full screen, which can be a full screen box. + +A box has content. Each content type is a context. So we can have - + Plain scrollable text view. + Fancy text view (hex and so on). + Text edit. + Directory browse. + Script controlled. + +Popup widgets centered on box/boxes that they affect. + Labels, Text line (with optional history), check boxes, radio buttons, OK/Cancel/etc buttons, popup select list (useful for history to, can be just a menu specilization). + Widget sets, though try to keep things simple enough to not need them. + Notifications, with the addition of an abort button. + If a single widget in the popup, prompt in a single line "box" near the bottom (like nano). + Options. + Keep options down to a bare minimum. + Command params. + Fetch command params from internal toybox structures, map them to the appropriate widget. + F2 menu - list of commands to apply to file/s or current directory. + The list is selectable by file type. + Should use the same code as the menu box, and allow sub menus. + Context sensitive history lists for selecting stuff. + Could also be a widget inside the popup when needed. + Search / replace. + Delete / save confirmation. + Command progress meter. + Use xargs, and have it output something useful per 'argument' for the progress meter. + xargs has an option to print the command to stdout, and to ask for confirmation per command. + xargs can run multiple threads. + +Scripting, so we can make things fancy and tie it together. + Don't forget to make it 'scriptable' via internal C. + MC uses that for the F2 menu, user menu, and archive access. + We should use scripts to define most of the above. + I'd like to use Lua, but shell is likely a better choice. + Coz toybox will have a shell. + And MC scripts are mostly shell bits. + Should have an actual toyboxes command that you can feed toyboxes scripts into. + Might actually get away with using that to define most of MC, AND be the editor plugins. + See how far I get, but that's what I'll start with for testing. + I should reuse the old emu protocol for this. + Not gonna actually sort function names or key combos. + A linear search is good enough for keys, they only come in at human speeds. + People might want the scripts to run faster, but in toybox we strive for simplicity. + Well, maybe a binary search within each modules function block that is sorted by hand in the source code. + On the other hand, might be able to make use of the toybox command parsing infrastructure for script functions. + Leaving that as a problem for toybox itself. + But it's not reusable, it uses globals, and not sure if we can screw with those globals. + +NOTE - toybox is designed to only deal with one command per process. So we can't call other toybox commands internally. + Or can we? Toysh does it. + +Events + We need a time event. Top has a resolution of hundredths of a second, though says that only tenths is officially supported. + Tenths makes sense for human speed UI. Hundredths makes sense if you want video frame rates. lol + Termios only allows tenths of seconds for read anyway. + Hmm, seems read() wants to wait for at least one byte, it's a between byte counter. Doh! + Select() is likely less simple, poll() the same, and epoll() seems to be linux specific. All allow more precise timeouts. + On the other hand, Rob is using poll() in netcat. + +Common bits / differences. + Initial toybox command arguments. + Ability to change those within the editor. + Files - passed as arguments, or can add / remove them from the running editor. + Process the file through some proggy, or just some function of the editor script. + Save / save as / backups. + Modelines are actualy discouraged as a security issue by the standard, but encouraged by toybox to use vi modelines. + Filename completion. + Filename prompts could have a couple of features. + Can pass things in and out of a shell command, append to the file, edit a specific fixed section of a file or special file (disk block editing!), or use stdin and stdout (joe in a pipe). + Directory / file browsing in a window. + Windows per current design. + Multiple files in the command line each have their own window. + Different / same file in each. + Each has it's own cursor / marks / block, etc. + Delete / scroll some other window. + Open a file in some other window, possibly creating one first. + Show one window full screen. + Method to show some hidden window, can be hidden if there's not enough space for them all. + Buffers - holds the contents of the files being edited / viewed. + Attached to windows, but can shift them around? + Edit / view / read only, named / unnamed buffers. + Special purpose buffers. + Emacs has - scratch, help, grep, compile, gdb, man, shell, probably others. + Though most of those are just running some other command in a window. + Kill ring buffer. + Kill goes to the buffer, delete just vanishes, on the other hand, this is really the difference between "cut" and "delete". + "Yank" just means "paste from kill buffer" then. + Emacs can have different working directory for each buffer, OR one global directory. + List them, perform some operation on the members of the list. Go through them all, prompting to save modified buffers. + Display text - navigate within it, scroll it in various ways. + Many ways to display otherwise unprintable text. + Inverted video for high bit characters. + Just show high bit characters. + UTF8. + ^X + Could be a problem with "where's my cursor" for the code. + Hex mode, wrap mode, raw / parsed mode, as well as formatted / unformatted mode. + Parsed mode has the text being processed by some command specified in the config file. + Formatted mode converts common formatting stuff to bold / underline. + Line numbers. + Scrolling can be definable amounts. + Some editors count buffer lines, some display lines. + Move to top, bottom, middle of screen / line. + Marks. + One mark and cursor. + Hmmm, Emacs uses the idea of a "point" which is between characters, with the "cursor" on the right side. + Not sure if this will be a problem. + Emacs has only one mark, and everything between point and mark is the "region", a block I think. + Multiple marks - numbered, named, just arbitrary, automated marks for various reasons. Line / line and character marks. + Next / previous / goto / remove one / all marks. + Whitespace / word boundaries / line / paragraph / "sections", etc. Should be definable. + Vi has multiples types of all of them. Pffft + Smooth scrolling (line by line). + Slow serial line support - do we need it? + Maybe. B-( + Status line. + Show cursor position, details of file / character under cursor / working directory. Often used for line input of parameters to. + Top of screen, bottom, above the key display in nano. Can have left, middle, right widgets. + Nano has essentially two status lines. + Expert mode to turn it off, disable it for more screen space. + Regexs - basic / extra / extended + Replacable stuff in search & replace. Ex/vi has this as an option. + Commands - invoked immediately with no echo, or typed via readline. + Pre command numbers (usually not echoed) / post command arguments. Also pre command regexs and other things to select the lines to work on. + Methods of repeating commands. Repeat last / next command, possibly X times. + Direction / motion. + Execute line / buffer / file. + Select lines, apply command to them. + Parameter expansion. + Key handling. + Bind / learn keys. + Emacs has "keymaps" for major and minor modes, as well as a global one. + Unbind keys. Mask keys from a lower level, but without actually binding them to anything, a NOP. + Command keys can be one or more keys. + Emacs and wordstar generally use a control key followed by some other key. + Show help page in a window with key bindings. Show binding for specific key. + Shortcut keys display. Nano has two lines of 6 each, showing only the most common. MC has one line, showing 10 function keys. No one else cares? + Esc key same as Alt / Meta key OR used for function keys OR used by itself. sigh + Meta key used to insert high bit characters. + Abort current command key. + Emacs has a keymap per buffer, which is the keybindings. + A global keymap. + The buffers major mode keymap. + Minor modes can have keymaps that override the major mode keymap when the minor mode is turned on. + Del <-> BS swapping. + Readline type widget. Called "minibuffer" in emacs. + For command parameters, also just go to one at the bottom to type commands into. + A fullscreen editor could be considered to just be a stack of these. + History, completion, editing, escape from. Position and length. + Same editing keys, and treat it just like a one line window. + Restricted or different editing keys while in some random line editing mode. + Moving readline to some other line (the basis of full screen editing perhaps). + Perhaps allow it to autoexpand if the input covers multiple lines. + Though that might be better to do as just creating more lines, then moving the readline between them. + Still would need the prompt on the top one, and to know to send them all at once when done. + How exactly does one create multiple lines? + Only way that makes sense is if the ENTER key is different from the "now do this" key. + Or wrapping long lines instead of scrolling them. + The prompt could include the default in (). + Mouse support - left click, double click, scroll wheel. + Only used to click on a widget, move cursor, or scroll around. + Shift click is used for X cut and paste support, think that just works from the terminal. + Shell - In a window / full screen. + Make editor a background task with bg / fg standard shell stuff. + Pass a block or buffer as stdin. Append / insert stdout to the buffer. + The shell output can just appended to the window contents as text that can be edited like every other window. When the cursor is at the bottom of the shell windov, stuff typed in is the next shell command. + Simple editing. + Move cursor, insert, delete, overwrite. + Basic editing. + Modes. + Vi has a lot of them - command, insert, ex, and moooore. + Emacs has definable modes that are a different concept to vi. + There are "major" and "minor" modes per buffer. + Major modes is for file type specific editing like "editing C, latex, etc" or "Dired". + Includes things like indenting, syntax highlighting, function boundaries (for ctags I guess), invoking the right compiler, keymaps, etc. + Can include other specialisations, like a python shell, and dired. + Minor modes are extras you can add, they seem to be things like "autofill", "wrap", "overwrite", "line numbers", etc. + Insert / overwrite mode. + Automatic detection of file type (typically source files) to put editor into different modes. + Cut, copy, paste, delete - blocks, word / line /etc, to end / beginning line, others. + To / from other buffers, files, "clipfile", maybe even the clipboard/s (thought that's an X thing I think). + Blocks - cut, copy, move, write to file. + Unhighlight block. + Search / replace - regex / shell glob / straight text / whole words. Case / charset sensitive. Forward / backward. + Regexs - basic / extra / extended + File / all files / within selection. Wrap around searching. + Incremental. Interactive / all replace. Inverted (find non matches). + Highlight / filter found. + Regex for the replace bit. Replacable stuff in search & replace. Ex/vi has this as an option. + Can search mixed hex and strings. + History, repeat, repeat in other direction. + Modified state. + Can be used by some commands to pester the user. + Ability to turn that state off. + Tabs / spaces / half tabs. Setting tab size. + Advanced editing. + Insert a character that is otherwise hard to insert, like command keys. + Insert date/time. + Quick search for a character in current line / forward / back. + "Smart" home, home goes to actual beginning, or first non blank. + Replace lots of space horizontally / vertically with just one / zero spaces. + Insert empty line/s above / below. + End of line space removal. + Allow cursor beyond end of line. + Visible white space. + DOS / Mac line ending convert. On the other hand, might just be good to do that transparently, remember on read, convert on save. + Change text encoding. + Add a newline at end of file if it's missing one. + Think we are doing that anyway, but an option to turn that off might be good. + Transpose, upper, lower, reverse case, capitalize words. + Adding a prefix / suffix string to selected lines. + Indent / outdent / line wrapping / centreing which can be auto / manual. Left and right margins. + Column blocks. + Deleting might just replace the column block with spaces / tabs. + Macros. Various ways of defining / invoking a macro. + Nested macros. + Auto expanding abbreviations. + Auto save after certain commands, or certain time. + Multi level undo / redo. Also undo current line (revert it?). + Disable undo. + Ability to list the undo "records"? + Emacs undo boundaries? + Spell checker. + Pretty printer (likely via shell command like indent). + Email quote handling. + Count / highlight lines matching or not matching regexes. + Sort block. + Persistant cursor position and selection. + Complete the word being typed based on other words in the file. + Code editing. + Ctags - basically lookup a symbol (word cursor is on) in the ctags files, which gives you a position in some other file that defines this symbol. Then display this other file somehow, probably allowing editing. + Ctags is in the standards, but do we want to write a toybox version? + Bracket / character matching. Goto / highlight matching bracket. Auto insert the closing one. + Include "insert one, blink the other". + Auto detect code block for indenting? + Syntax highlighting. + Next / previous error (compile errors usually). + +Readline. + toybox has get_line() and get_rawline(), but they are not interactive readlines. + In order to have our readline() be useful for generic use, the keystrokes that do stuff must be definable. + So, what do we need? + GNU readline has (leaving out a few things) - + Editing + How is a "word" defined?. + Hitting ENTER anywhere in the line submits it. An editor would want to actually insert the ENTER into the text. + Move cursor / insert / delete / backspace / undo (all the way back) / revert. + Perhaps redo might be nice. + Move cursor to start / end of line, forward / back one word, redraw. + Cut / copy / paste. Kill ring with ring rotation? + Entire line. + All spaces around cursor. + From cursor to mark. + Highlighted block. + From cursor to start / end of line. + to end of word, or end of next word if between words. + to beginning of word, or of the previous word if between words. + to previous white space. Different from above coz apparently white space is different from word boundaries. shrugs + Numbers in front of commands. Vi and emacs probably need this? More and less "needs" this. Otherwise... eww. + For repeats, or sometimes to reverse the direction if it's negative. + Insert / overwrite mode. + Upper and lower casing characters or words. + Transpose characters / words. + Marks - setting a mark and moving the cursor to it / swapping with it. + Character search within the string - forward / back. + Insert comment - bloat we don't need. + History. + Back / forward one line. Goto start / end of history. + Incremental and non incremental searching, forwards or back. + During incremental search - ability to abort and restore original line. + Remember the last (incremental?) search. + Either return the found history (ENTER), or allow editing (editing keys). + Option to save edited history lines, and mark them as edited. + Show key bindings / macros / etc. + Tab completion. + List / select completions. + Cycle through the matches. + Macros! + Config files. + Not likely to emulate these, we have our own needs, and it's not a standard. + Expansions. + MC will want argument expansions at least. + Though perhaps this is best left to the code that calls this to expand the result. + +more + Can handle switch between multiple files. + Searches are regexs. Can also search for non matches. + Multiple marks. + Forward and back half screenful, with "half" being the specified number, but defaulting to actual half. + Shell commands (not in the standard). + Invoke the editor mentioned in EDITOR, or default to vi. Pass line number if it's vi. + Print info about file. + Ctags. + Some commands are single letters, some are ":" followed by a single letter, possible with more text, then ENTER. + Or other variations. + +less + Has bracket matching, but only from top-open / bottom-close. + Can search between multiple files, or add more. + Files on the command line, add / remove files, just open a new file now. + Highlight found text. + Filter found lines. + Option to NOT do regex search. + Change the command line arguments while running. + Shell commands with replacable params. + Input processor - some proggy that the input file is processed through, the output of that is shown. + Um, why not use pipes and input redirection? + +ed - obsolete for toybox, ancestor of ex. + A line editor. + In command mode, type the command, then ENTER. + In input mode, type text, with "." on a line by itself to go back to command mode. + Uses one or two optional line addresses (can be regex) followed by a single character command, then arguments. + The "address" can be various symbols with various meanings, see the manual. + Usual basic editing and navigation commands. + Join lines. + Mark lines. + Display lines (with line numbers). + Copy / move lines. + Apply a command to a bunch of lines matching / not matching the address. + Insert a file / write lines to a file. + Search and replace. + Undo. + Shell command, with filename replacement character. + +sed + Stream editor. + Fairly similar to ed, except it applies a script of commands to each line in turn. + Branch to a label within the script. Can test if a substitution happened before deciding to branch. + Includes a "hold space" for cut and paste type operations, as well as swapping. + Can output line numbers. + Can read in a file, or write to one. + Can be commented. + +ex - obsolete for toybox, but part of vi. + A line editor, apparently the line oriented editing mode for vi. + So while the command itself is obsolete, it's internal stuff might be needed for vi. + In fact a lot of the standard for vi just refers to ex. + ":" means to perform an ex command from vi. lol + Starts in command mode (":" prompt). + Basically a "type command then ENTER" command mode. + Text input mode (append, insert, change) ended by "." on it's own line. + Has line addresses before the commands, similar to ed. + Commands have complex parsing requirements. Ewww. + Has some really basic command line editing. + Has a bunch of buffers, with modes. Commands apply to a named buffer, or the unnamed one if no name is given. + Abbreviations and maps. Same thing? They interact. lol + Think the difference is that maps don't have to have blank space after them. + They expand during typing. + Seems that maps are for binding to keys? + Set various options. + Shell command, with optional interaction. + Read commands from a file. + Can scan ctags files looking for regexs. + Can switch to "open" and "visual" modes, whatever they are. + "Visual" mode is just vi. + "Open" mode I think is a vi for dumb terminals? + Has a concept of "window", also not sure what that is. Might just be the number of terminal lines. + Shell escape - pass selected lines to a shell command, replace them with whatever it returns. + Shift lines back and forth. Indent and outdent in other words. + Execute a buffer as ex commands. + Regexs have extra thingies. + Replace commands can refer to other bits of text using parameters. See the manual. + Autowrite - basicaly save the file after certain commands. + Mode that strips out non printables on file read. + Can be made ed compatible. lol + Can display line numbers. + Paragraph boundary pairs can be set. + Definable scroll distance. + Visual and open modes have "sections", with definable pairs of boundary characters. + Can show matching braces. + Can optionally warn on some commands if the buffers are modified. + Margin auto wrap and end of line blanks removal. + Can wrap searches. + "modelines" (as used at the top of toybox source files) are apparently is strongly discouraged by the standard. shrugs + Overlaping copies are allowed. + Automatic marks created sometimes. + +vi + Notable for it's variety of modes, and it's standard command mode that uses non echoed ordinary keys for common commands. + "Visual" editor, a screen oriented superset of ex, that lets you use ex commands. + A lot of the standard simply refers to ex. + Seems to mostly be similar to ex commands, only done full screen, and with full screen navigation. + Has "open" and "visual" modes, but not sure what they are. Also "ex" mode, with means using ex commands. + I think "open" mode is for dumb terminals, it's all done on the bottom line, perhaps with full screen redraws. + "Visual" mode then must be full screen editing. + Text input mode can return to command mode with Esc. + Has five kinds of "words" and four kinds of "bigwords", six types of "sections", four types of "paragraphs", and three types of "sentences". + There can be a multi digit count before commands, it's not echoed anywhere when not in a command line mode, the command key then does its command count times. + For instance typing "12h" does not echo anything, but just moves the cursor back 12 places when the "h" is hit. + Lots of those seem to be letters, or control keys, though there are others. + [count] ! motion shell-commands\n"); + else + reply->addstrf(reply, "Session:
\n\n"); + + if (NULL != shs->name) + reply->addstrf(reply, " name = %s\n", shs->name); + if (NULL != shs->UUID) + reply->addstrf(reply, " UUID = %s\n", shs->UUID); + reply->addstrf(reply, " salt = %s\n", shs->salt); + reply->addstrf(reply, " seshID = %s\n", shs->seshID); + reply->addstrf(reply, " timeStamp = %ld.%ld\n", shs->timeStamp[1].tv_sec, shs->timeStamp[1].tv_nsec); + reply->addstrf(reply, " sesh = %s\n", shs->sesh); + reply->addstrf(reply, " munchie = %s\n", shs->munchie); + reply->addstrf(reply, " toke_n_munchie = %s\n", shs->toke_n_munchie); + reply->addstrf(reply, " hashish = %s\n", shs->hashish); + reply->addstrf(reply, " leaf = %s\n", shs->leaf); + reply->addstrf(reply, " level = %d\n", (int) shs->level); + reply->addstr(reply, "\n"); +} + + +char toybuf[4096]; +lua_State *L; +qhashtbl_t *configs; +MYSQL *database, *dbconn; +unsigned int dbTimeout; +struct timespec dbLast; +my_bool dbReconnect; +gridStats *stats; +boolean isTmux = 0; +boolean isWeb = 0; +char *pwd = ""; +char *scRoot = "/opt/opensim_SC"; +char *scUser = "opensimsc"; +char *scBin = ""; +char *scEtc = ""; +char *scLib = ""; +char *scRun = ""; +char *scBackup = ""; +char *scCache = ""; +char *scData = ""; +char *scLog = ""; +char *Tconsole = "SledjChisl"; +char *Tsocket = "opensim-tmux.socket"; +char *Ttab = "SC"; +char *Tcmd = "tmux -S"; +char *webRoot = "/var/www/html"; +char *URL = "fcgi-bin/sledjchisl.fcgi"; +char *ToS = "Be good."; +char *webIframers = ""; +int seshRenew = 10 * 60; +int idleTimeOut = 30 * 60; +int seshTimeOut = 24 * 60 * 60; +int newbieTimeOut = 30; +float loadAverageInc = 0.5; +int simTimeOut = 45; +boolean DEBUG = TRUE; +qhashtbl_t *mimeTypes; +qlist_t *dbRequests; + + +// TODO - log to file. The problem is we don't know where to log until after we have loaded the configs, and before that we are spewing log messages. +// Now that we are using spawn-fcgi, all the logs are going to STDERR, which we can capture and write to a file. +// Unfortunately spawn-fcgi in deamon mode sends all the output to /dev/null or something. +// A better idea, when we spawn tmux or spawn-fcgi, capture STDERR, full log everything to that, filtered log to the tmux console (STDOUT). +// Then we can use STDOUT / STDIN to run the console stuff. + +// TODO - escape anything that will turn the console into garbage. + +// https://stackoverflow.com/questions/4842424/list-of-ansi-color-escape-sequences +char *logTypes[] = +{ + "91;1;4", "CRITICAL", // red underlined + "31", "ERROR", // dark red + "93", "WARNING", // yellow + "36", "TIMEOUT", // cyan + "97;40", "INFO", // white + "90", "DEBUG", // grey +// VERBOSE? UNKNOWN? FATAL? SILENT? All from Android aparently. + "35", "debug", // magenta + "34", "timeout", // blue +}; + +#define DATE_TIME_LEN 42 +void logMe(int v, char *format, ...) +{ + va_list va, va2; + int len; + char *ret; + struct timeval tv; + time_t curtime; + char date[DATE_TIME_LEN]; + + va_start(va, format); + va_copy(va2, va); + // How long is it? + len = vsnprintf(0, 0, format, va); + len++; + va_end(va); + // Allocate and do the sprintf() + ret = xmalloc(len); + vsnprintf(ret, len, format, va2); + va_end(va2); + + gettimeofday(&tv, NULL); + curtime = tv.tv_sec; + strftime(date, DATE_TIME_LEN, "(%Z %z) %F %T", localtime(&curtime)); + + v *= 2; + fprintf(stderr, "%s.%.6ld \e[%sm%-8s sledjchisl: %s\e[0m\n", date, tv.tv_usec, logTypes[v], logTypes[v + 1], ret); + free(ret); +} +#define C(...) logMe(0, __VA_ARGS__) +#define E(...) logMe(1, __VA_ARGS__) +#define W(...) logMe(2, __VA_ARGS__) +#define T(...) logMe(3, __VA_ARGS__) +#define I(...) logMe(4, __VA_ARGS__) +#define D(...) logMe(5, __VA_ARGS__) +#define d(...) logMe(6, __VA_ARGS__) +#define t(...) logMe(7, __VA_ARGS__) + + +static void addStrL(qlist_t *list, char *s) +{ + list->addlast(list, s, strlen(s) + 1); +} + +static char *getStrH(qhashtbl_t *hash, char *key) +{ + char *ret = "", *t; + + t = hash->getstr(hash, key, false); + if (NULL != t) + ret = t; + return ret; +} + + +char *myHMAC(char *in, boolean b64) +{ + EVP_MD_CTX *mdctx = EVP_MD_CTX_create(); // Gets renamed to EVP_MD_CTX_new() in later versions. + unsigned char md_value[EVP_MAX_MD_SIZE]; + unsigned int md_len; + + EVP_DigestInit_ex(mdctx, EVP_sha512(), NULL); // EVP_sha3_512() isn't available until later versions. + EVP_DigestUpdate(mdctx, in, strlen(in)); + EVP_DigestFinal_ex(mdctx, md_value, &md_len); + EVP_MD_CTX_destroy(mdctx); // Gets renamed to EVP_MD_CTX_free() in later versions. + + if (b64) + return qB64_encode(md_value, md_len); + else + return qhex_encode(md_value, md_len); +} + +char *myHMACkey(char *key, char *in, boolean b64) +{ + unsigned char md_value[EVP_MAX_MD_SIZE]; + unsigned int md_len; + unsigned char* digest = HMAC(EVP_sha512(), key, strlen(key), (unsigned char *) in, strlen(in), md_value, &md_len); + + if (b64) + return qB64_encode(md_value, md_len); + else + return qhex_encode(md_value, md_len); +} + + +// In Lua 5.0 reference manual is a table traversal example at page 29. +void PrintTable(lua_State *L) +{ + lua_pushnil(L); + + while (lua_next(L, -2) != 0) + { + // Numbers can convert to strings, so check for numbers before checking for strings. + if (lua_isnumber(L, -1)) + printf("%s = %f\n", lua_tostring(L, -2), lua_tonumber(L, -1)); + else if (lua_isstring(L, -1)) + printf("%s = '%s'\n", lua_tostring(L, -2), lua_tostring(L, -1)); + else if (lua_istable(L, -1)) + PrintTable(L); + lua_pop(L, 1); + } +} + + +int sendTmuxKeys(char *dest, char *keys) +{ + int ret = 0, i; + char *c = xmprintf("%s %s/%s send-keys -t %s:%s '%s'", Tcmd, scRun, Tsocket, Tconsole, dest, keys); + + i = system(c); + if (!WIFEXITED(i)) + E("tmux send-keys command failed!"); + free(c); + return ret; +} + +int sendTmuxCmd(char *dest, char *cmd) +{ + int ret = 0, i; + char *c = xmprintf("%s %s/%s send-keys -t %s:'%s' '%s' Enter", Tcmd, scRun, Tsocket, Tconsole, dest, cmd); + + i = system(c); + if (!WIFEXITED(i)) + E("tmux send-keys command failed!"); + free(c); + return ret; +} + +void waitTmuxText(char *dest, char *text) +{ + int i; + // Using " for the grep pattern, coz ' might be used in a sim name. +// TODO - should escape \ " ` in text. + 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); + + D("Waiting for '%s'.", text); + do + { + i = system(c); + if (!WIFEXITED(i)) + { + E("tmux capture-pane command failed!"); + break; + } + else if (0 == WEXITSTATUS(i)) + break; + } while (1); + + free(c); +} + +float waitLoadAverage(float la, float extra, int timeout) +{ + struct sysinfo info; + struct timespec timeOut; + float l; + int to = timeout; + + T("Sleeping until load average is below %.02f (%.02f + %.02f) or for %d seconds.", la + extra, la, extra, timeout); + clock_gettime(CLOCK_MONOTONIC, &timeOut); + to += timeOut.tv_sec; + + do + { + msleep(5000); + sysinfo(&info); + l = info.loads[0]/65536.0; + clock_gettime(CLOCK_MONOTONIC, &timeOut); + timeout -= 5; + t("Tick, load average is %.02f, countdown %d seconds.", l, timeout); + } while (((la + extra) < l) && (timeOut.tv_sec < to)); + + return l; +} + + +// Rob forget to do this, but at least he didn't declare it static. +struct dirtree *dirtree_handle_callback(struct dirtree *new, int (*callback)(struct dirtree *node)); + +typedef struct _simList simList; +struct _simList +{ + int len, num; + char **sims; +}; + +static int filterSims(struct dirtree *node) +{ + if (!node->parent) return DIRTREE_RECURSE | DIRTREE_SHUTUP; + if ((strncmp(node->name, "sim", 3) == 0) && ((strcmp(node->name, "sim_skeleton") != 0))) + { + simList *list = (simList *) node->parent->extra; + + if ((list->num + 1) > list->len) + { + list->len = list->len + 1; + list->sims = xrealloc(list->sims, list->len * sizeof(char *)); + } + list->sims[list->num] = xstrdup(node->name); + list->num++; + } + return 0; +} + +// We particularly don't want \ " ` +char *cleanSimName(char *name) +{ + size_t l = strlen(name); + char *ret = xmalloc(l + 1); + int i, j = 0; + + for (i = 0; i < l; i++) + { + char r = name[i]; + + if ((' ' == r) || (isalnum(r) != 0)) + ret[j++] = r; + } + ret[j] = '\0'; + + return ret; +} + +simList *getSims() +{ + simList *sims = xmalloc(sizeof(simList)); + memset(sims, 0, sizeof(simList)); + char *path = xmprintf("%s/config", scRoot); + struct dirtree *new = dirtree_add_node(0, path, 0); + new->extra = (long) sims; + dirtree_handle_callback(new, filterSims); + + qsort(sims->sims, sims->num, sizeof(char *), qstrcmp); + free(path); + return sims; +} + +void freeSimList(simList *sims) +{ + int i; + + for (i = 0; i < sims->num; i++) + free(sims->sims[i]); + free(sims->sims); + free(sims); +} + +static int filterInis(struct dirtree *node) +{ + if (!node->parent) return DIRTREE_RECURSE | DIRTREE_SHUTUP; + int l = strlen(node->name); + if (strncmp(&(node->name[l - 4]), ".ini", 4) == 0) + { + strcpy((char *) node->parent->extra, node->name); + return DIRTREE_ABORT; + } + return 0; +} + +char *getSimName(char *sim) +{ + char *ret = NULL; + char *c = xmprintf("%s/config/%s", scRoot, sim); + struct dirtree *new = dirtree_add_node(0, c, 0); + + free(c); + c = xzalloc(1024); + new->extra = (long) c; + dirtree_handle_callback(new, filterInis); + if ('\0' != c[0]) + { + char *temp = NULL; + regex_t pat; + regmatch_t m[2]; + long len; + int fd; + + temp = xmprintf("%s/config/%s/%s", scRoot, sim, c); + fd = xopenro(temp); + xregcomp(&pat, "RegionName = \"(.+)\"", REG_EXTENDED); + do + { + // TODO - get_line() is slow, and wont help much with DOS and Mac line endings. + // gio_gets() isn't any faster really, but deals with DOS line endings at least. + free(temp); + temp = get_line(fd); + if (temp) + { + if (!regexec(&pat, temp, 2, m, 0)) + { + // Return first parenthesized subexpression as string. + if (pat.re_nsub > 0) + { + ret = xmprintf("%.*s", (int) (m[1].rm_eo - m[1].rm_so), temp + m[1].rm_so); + free(temp); + break; + } + } + } + } while (temp); + regfree(&pat); + xclose(fd); + } + free(c); + return ret; +} + + +// Expects either "simXX" or "ROBUST". +int checkSimIsRunning(char *sim) +{ + int ret = 0; + struct stat st; + + // Check if it's running. + char *path = xmprintf("%s/caches/%s.pid", scRoot, sim); + if (0 == stat(path, &st)) + { + int fd, i; + char *pid = NULL; + + // Double check if it's REALLY running. + if ((fd = xopenro(path)) == -1) + perror_msg("xopenro(%s)", path); + else + { + pid = get_line(fd); + if (NULL == pid) + perror_msg("get_line(%s)", path); + else + { + xclose(fd); + +// I'd rather re-use the toysh command running stuff, since ps is a toy, but that's private. +// TODO - switch to toybox ps and rm. + free(path); + path = xmprintf("ps -p %s --no-headers -o comm", pid); + i = system(path); + if (WIFEXITED(i)) + { + if (0 != WEXITSTATUS(i)) // No such pid. + { + free(path); + path = xmprintf("rm -f %s/caches/%s.pid", scRoot, sim); + d("%s", path); + i = system(path); + } + else + d("checkSimIsRunning(%s) has PID %s, which is actually running.", sim, pid); + } + } + } + free(pid); + } + + // Now check if it's really really running. lol + free(path); + path = xmprintf("%s/caches/%s.pid", scRoot, sim); + if (0 == stat(path, &st)) + { + D("checkSimIsRunning(%s) -> %s is really really running.", sim, path); + ret = 1; + } + else + D("checkSimIsRunning(%s) -> %s is not running.", sim, path); + + free(path); + return ret; +} + + +static void PrintEnv(qgrow_t *reply, char *label, char **envp) +{ + reply->addstrf(reply, "%s:
\n\n", label); + for ( ; *envp != NULL; envp++) + reply->addstrf(reply, "%s\n", *envp); + reply->addstr(reply, "\n"); +} + +static void printEnv(char **envp) +{ + for ( ; *envp != NULL; envp++) + D("%s", *envp); +} + + +typedef struct _rowData rowData; +struct _rowData +{ + char **fieldNames; + qlist_t *rows; +}; + +static void dumpHash(qhashtbl_t *tbl) +{ + qhashtbl_obj_t obj; + + memset((void*)&obj, 0, sizeof(obj)); + tbl->lock(tbl); + while(tbl->getnext(tbl, &obj, true) == true) + d("%s = %s", obj.name, (char *) obj.data); + tbl->unlock(tbl); +} + +static void dumpArray(int d, char **ar) +{ + int i = 0; + + while (ar[i] != NULL) + { + d("%d %d %s", d, i, ar[i]); + i++; + } +} + + + +typedef struct _dbFields dbFields; +struct _dbFields +{ + qlisttbl_t *flds; + int count; +}; +typedef struct _dbField dbField; +struct _dbField +{ + char *name; + enum enum_field_types type; + unsigned long length; + unsigned int flags; + unsigned int decimals; +}; + +void dbFreeFields(dbFields *flds, boolean all) +{ + flds->count--; + +// TODO - sigh, looks to be inconsistant why some do and some don't leak. +// I guess the ones that don't leak are the ones that crash? +// It's only a tiny leak anyway, 80 bytes total. +// if ((0 >= flds->count) || all) // CRASHY + if ((0 >= flds->count)) // LEAKY + { + qlisttbl_obj_t obj; + + memset((void *) &obj, 0, sizeof(obj)); + flds->flds->lock(flds->flds); + while(flds->flds->getnext(flds->flds, &obj, NULL, false) == true) + { + dbField *fld = (dbField *) obj.data; + free(fld->name); + } + flds->flds->unlock(flds->flds); + flds->flds->free(flds->flds); + flds->flds = NULL; + free(flds); + } +} + +enum dbCommandType +{ + CT_SELECT, + CT_CREATE, + CT_UPDATE, + CT_NONE +}; + +typedef struct _dbRequest dbRequest; +struct _dbRequest +{ + char *table, *join, *where, *order, *sql; + MYSQL_STMT *prep; // NOTE - executing it stores state in this. + dbFields *fields; + int inCount, outCount, rowCount; + char **inParams, **outParams; + MYSQL_BIND *inBind, *outBind; + rowData *rows; + my_ulonglong count; + enum dbCommandType type; + boolean freeOutParams; +}; + +void dbFreeRequest(dbRequest *req, boolean all) +{ + int i; + + D("Cleaning up prepared database request %s - %s %d %d", req->table, req->where, req->outCount, req->inCount); + + if (NULL != req->outBind) + { + for (i = 0; i < req->outCount; i++) + { + if (NULL != req->outBind[i].buffer) free(req->outBind[i].buffer); + if (NULL != req->outBind[i].length) free(req->outBind[i].length); + if (NULL != req->outBind[i].error) free(req->outBind[i].error); + if (NULL != req->outBind[i].is_null) free(req->outBind[i].is_null); + } + free(req->outBind); + req->outBind = NULL; + } + else + D(" No out binds to clean up for %s - %s.", req->table, req->where); + if (NULL != req->inBind) + { + for (i = 0; i < req->inCount; i++) + { + if (NULL != req->inBind[i].buffer) free(req->inBind[i].buffer); + if (NULL != req->inBind[i].length) free(req->inBind[i].length); + if (NULL != req->inBind[i].error) free(req->inBind[i].error); + if (NULL != req->inBind[i].is_null) free(req->inBind[i].is_null); + } + free(req->inBind); + req->inBind = NULL; + } + else + D(" No in binds to clean up for %s - %s.", req->table, req->where); + + if (req->freeOutParams && all) + { + if (NULL != req->outParams) + { + free(req->outParams); + req->outParams = NULL; + } + else + D(" No out params to clean up for %s - %s.", req->table, req->where); + } + if (NULL != req->sql) free(req->sql); + else + D(" No SQL to clean up for %s - %s.", req->table, req->where); + req->sql = NULL; + if (NULL != req->prep) + { + if (0 != mysql_stmt_close(req->prep)) + C(" Unable to close the prepared statement!"); + req->prep = NULL; + } + + if (all) + { + if (NULL != req->fields) + { + dbFreeFields(req->fields, all); + req->fields = NULL; + } + else + D(" No fields to clean up for %s - %s.", req->table, req->where); + } +} + +void freeDb(boolean all) +{ + dbRequest **rq; + + if (dbRequests) + { + if (all) + { + while (NULL != (rq = (dbRequest **) dbRequests->popfirst(dbRequests, NULL))) + { + dbFreeRequest(*rq, all); + free(rq); + } + dbRequests->free(dbRequests); + dbRequests = NULL; + } + else + { + qlist_obj_t obj; + + memset((void*)&obj, 0, sizeof(obj)); // must be cleared before call + dbRequests->lock(dbRequests); + while (dbRequests->getnext(dbRequests, &obj, false) == true) + dbFreeRequest(*((dbRequest **) obj.data), all); + dbRequests->unlock(dbRequests); + } + } + + if (database) mysql_close(database); + database = NULL; + mysql_library_end(); +} + +static boolean dbConnect() +{ + database = mysql_init(NULL); + if (NULL == database) + { + E("mysql_init() failed - %s", mysql_error(database)); + return FALSE; + } + +/* TODO - dammit, no mysql_get_option(), MariaDB docs say mysql_get_optionv(), which doesn't exist either. + Says "This function was added in MariaDB Connector/C 3.0.0.", I have MariaDB / MySQL client version: 10.1.44-MariaDB. + + if (mysql_get_option(database, MYSQL_OPT_CONNECT_TIMEOUT, &dbTimeout)) + E("mysql_get_option(MYSQL_OPT_CONNECT_TIMEOUT) failed - %s", mysql_error(database)); + else + D("Database MYSQL_OPT_CONNECT_TIMEOUT = %d", dbTimeout); + + if (mysql_get_option(database, MYSQL_OPT_RECONNECT, &dbReconnect)) + E("mysql_get_option(MYSQL_OPT_RECONNECT) failed - %s", mysql_error(database)); + else + D("Database MYSQL_OPT_RECONNECT = %d", (int) dbReconnect); +*/ + + // Seems best to disable auto-reconnect, so I have more control over reconnections. + dbReconnect = 0; + if (mysql_options(database, MYSQL_OPT_RECONNECT, &dbReconnect)) + E("mysql_options(MYSQL_OPT_RECONNECT) failed - %s", mysql_error(database)); + else + D("Database MYSQL_OPT_RECONNECT is now %d", (int) dbReconnect); + + dbconn = mysql_real_connect(database, + getStrH(configs, "Data Source"), + getStrH(configs, "User ID"), + getStrH(configs, "Password"), + getStrH(configs, "Database"), +// 3036, "/var/run/mysqld/mysqld.sock", + 0, NULL, + CLIENT_FOUND_ROWS | CLIENT_LOCAL_FILES | CLIENT_MULTI_STATEMENTS | CLIENT_MULTI_RESULTS); + if (NULL == dbconn) + { + E("mysql_real_connect() failed - %s", mysql_error(database)); + return FALSE; + } + + // Just set the fucking thing to a year. Pffft. + dbTimeout = 60 * 60 * 24 * 7 * 52; + char *sql = xmprintf("SET SESSION wait_timeout=%d", (int) dbTimeout); + + if (mysql_query(database, sql)) + E("SET SESSION wait_timeout=%d failed - %s", (int) dbTimeout, mysql_error(database)); + else + D("Database wait_timeout = %d", (int) dbTimeout); + free(sql); + + if (-1 == clock_gettime(CLOCK_REALTIME, &dbLast)) + perror_msg("Unable to get the time."); + + return TRUE; +} + +// A general error function that checks for certain errors that mean we should try to connect to the server MariaDB again. +// https://mariadb.com/kb/en/mariadb-error-codes/ +// 1129? 1152? 1184? 1218? 1927 3032? 4150? +// "server has gone away" isn't listed there, that's the one I was getting. Pffft +// It's 2006, https://dev.mysql.com/doc/refman/8.0/en/gone-away.html +// Ah it could be "connection inactive for 8 hours". +// Which might be why OpenSim opens a new connection for EVERYTHING. +// https://dev.mysql.com/doc/refman/5.7/en/c-api-auto-reconnect.html +// Has more details. +static boolean dbCheckError(char *error, char *sql) +{ + int e = mysql_errno(database); + + E("MariaDB error %d - %s: %s\n%s", e, error, mysql_error(database), sql); + if (2006 == e) + { + W("Reconnecting to database."); + freeDb(false); + return dbConnect(); + } + + return FALSE; +} +// "Statement execute failed 2013: Lost connection to MySQL server during query" +static boolean dbStmtCheckError(dbRequest *req, char *error, char *sql) +{ + int e = mysql_stmt_errno(req->prep); + + E("MariaDB prepared statement error %d - %s: %s\n%s", e, error, mysql_stmt_error(req->prep), sql); + if (2013 == e) + { + W("Reconnecting to database."); + freeDb(false); + return dbConnect(); + } + + return FALSE; +} + +dbFields *dbGetFields(char *table) +{ + static qhashtbl_t *tables = NULL; + if (NULL == tables) tables = qhashtbl(0, 0); + dbFields *ret = tables->get(tables, table, NULL, false); + + if (NULL == ret) + { + // Seems the only way to get field metadata is to actually perform a SQL statement, then you get the field metadata for the result set. + // Chicken, meet egg, sorry you had to cross the road for this. + char *sql = xmprintf("SELECT * FROM %s LIMIT 0", table); + +d("Getting field metadata for %s", table); + if (mysql_query(database, sql)) + { +// E("MariaDB error %d - Query failed 0: %s\n%s", mysql_errno(database), mysql_error(database), sql); + if (dbCheckError("Query failed 0", sql)) + { + ret = dbGetFields(table); + free(sql); + return ret; + } + } + else + { + MYSQL_RES *res = mysql_store_result(database); + + if (!res) + E("MariaDB error %d - Couldn't get results set from %s\n %s", mysql_errno(database), mysql_error(database), sql); + else + { + MYSQL_FIELD *fields = mysql_fetch_fields(res); + + if (!fields) + E("MariaDB error %d - Failed fetching fields: %s", mysql_errno(database), mysql_error(database)); + else + { + unsigned int i, num_fields = mysql_num_fields(res); + + ret = xmalloc(sizeof(dbFields)); // Little bit LEAKY + ret->flds = qlisttbl(QLISTTBL_UNIQUE | QLISTTBL_LOOKUPFORWARD); + ret->count = 1; + for (i = 0; i < num_fields; i++) + { + dbField *fld = xmalloc(sizeof(dbField)); + fld->name = xstrdup(fields[i].name); + fld->type = fields[i].type; + fld->length = fields[i].length; + fld->flags = fields[i].flags; + fld->decimals = fields[i].decimals; + ret->flds->put(ret->flds, fld->name, fld, sizeof(*fld)); + free(fld); + } + tables->put(tables, table, ret, sizeof(*ret)); + } + mysql_free_result(res); + } + } + free(sql); + } + else // Reference count these, coz some tables are used more than once. + ret->count++; + + return ret; +} + + +/* How to deal with prepared SQL statements. +http://karlssonondatabases.blogspot.com/2010/07/prepared-statements-are-they-useful-or.html +https://blog.cotten.io/a-taste-of-mysql-in-c-87c5de84a31d?gi=ab3dd1425b29 +https://raspberry-projects.com/pi/programming-in-c/databases-programming-in-c/mysql/accessing-the-database + +IG and CG now both have sims connected to other grids, so some sort of +multi database solution would be good, then we can run the grid and the +external sims all in one. + +Not sure if this'll work with Count(*). + +--------------------------------------------- + +The complicated bit is the binds. + +You are binding field values to C memory locations. +The parameters and returned fields need binds. +Mostly seems to be the value parts of the SQL statements. + +I suspect most will be of the form - + ... WHERE x=? and foo=? + INSERT INTO table VALUES (?,?,?) + UPDATE table SET x=?, foo=? WHERE id=? + + A multi table update - + UPDATE items,month SET items.price=month.price WHERE items.id=month.id; +*/ + +int dbDoSomething(dbRequest *req, boolean count, ...) +{ + int ret = 0; + va_list ap; + struct timespec then; + int i, j; + MYSQL_RES *prepare_meta_result = NULL; + + if (-1 == clock_gettime(CLOCK_REALTIME, &then)) + perror_msg("Unable to get the time."); + +// TODO - should factor this out to it's own function, and call that function in dbCount() and dbCountJoin(). +// Or better yet, finally migrate those functions to using dbDoSomething(). + double n = (dbLast.tv_sec * 1000000000.0) + dbLast.tv_nsec; + double t = (then.tv_sec * 1000000000.0) + then.tv_nsec; + +t("Database timeout test %lf > %lf", ((t - n) / 1000000000.0), (dbTimeout / 2.0)); + if (((t - n) / 1000000000.0) > (dbTimeout / 2.0)) + { + T("Avoid database timeout of %d seconds, pinging it.", dbTimeout); + if (0 != mysql_ping(database)) + { + W("Reconnecting to database."); + freeDb(false); + dbConnect(); + } + } + + va_start(ap, count); + + if (NULL == req->prep) + { + D("Creating prepared statement for %s - %s", req->table, req->where); + + if (0 == req->type) + req->type = CT_SELECT; + + req->fields = dbGetFields(req->table); + if (NULL == req->fields) + { + E("Unknown fields for table %s.", req->table); + ret++; + goto end; + } + + switch (req->type) + { + case CT_SELECT : + { + char *select = xmprintf(""); + + i = 0; + while (req->outParams[i] != NULL) + { + char *t = xmprintf("%s,%s", select, req->outParams[i]); + free(select); + select = t; + i++; + } + if (0 == i) + { + free(select); + if (count) + select = xmprintf(",Count(*)"); + else + select = xmprintf(",*"); + } + + if (NULL == req->join) + req->join = ""; + + if (req->where) + req->sql = xmprintf("SELECT %s FROM %s %s WHERE %s", &select[1], req->table, req->join, req->where); + else + req->sql = xmprintf("SELECT %s FROM %s", &select[1], req->table, req->join); + free(select); + if (req->order) + { + char *t = xmprintf("%s ORDER BY %s", req->sql, req->order); + + free(req->sql); + req->sql = t; + } + break; + } + + case CT_CREATE : + { + char *values = xmprintf(""); + + i = 0; + while (req->inParams[i] != NULL) + { + char *t = xmprintf("%s, %s=?", values, req->inParams[i]); + free(values); + values = t; + i++; + } + if (0 == i) + { + E("Statement prepare for INSERT must have in paramaters."); + ret++; + free(values); + values = xmprintf(""); + } + req->sql = xmprintf("INSERT INTO %s SET %s", req->table, &values[1]); + free(values); + + break; + } + + case CT_UPDATE : + { + break; + } + + case CT_NONE : + { + W("No SQL type!"); + break; + } + } + +d("New SQL statement - %s", req->sql); + // prepare statement with the other fields + req->prep = mysql_stmt_init(database); + if (NULL == req->prep) + { + E("Statement prepare init failed: %s\n", mysql_stmt_error(req->prep)); + ret++; + goto end; + } + if (mysql_stmt_prepare(req->prep, req->sql, strlen(req->sql))) + { + E("Statement prepare failed: %s\n", mysql_stmt_error(req->prep)); + ret++; + goto end; + } + + // setup the bind stuff for any "?" parameters in the SQL. + req->inCount = mysql_stmt_param_count(req->prep); + i = 0; + while (req->inParams[i] != NULL) + i++; + if (i != req->inCount) + { + E("In parameters count don't match %d != %d for - %s", i, req->inCount, req->sql); + ret++; + goto freeIt; + } + req->inBind = xzalloc(i * sizeof(MYSQL_BIND)); +//W("Allocated %d %d inBinds for %s", i, req->inCount, req->sql); + for (i = 0; i < req->inCount; i++) + { + dbField *fld = req->fields->flds->get(req->fields->flds, req->inParams[i], NULL, false); + + if (NULL == fld) + { + E("Unknown input field %d %s.%s for - %s", i, req->table, req->inParams[i], req->sql); + ret++; + goto freeIt; + } + else + { + // https://blog.cotten.io/a-taste-of-mysql-in-c-87c5de84a31d?gi=ab3dd1425b29 + // For some gotchas about all of this binding bit. + req->inBind[i].buffer_type = fld->type; + req->inBind[i].buffer = xzalloc(fld->length + 1); // Note the + 1 is for string types, and a waste for the rest. + req->inBind[i].buffer_length = fld->length + 1; + switch(fld->type) + { + case MYSQL_TYPE_TINY: + { +//d("TINY %d %s %d", i, fld->name, req->inBind[i].buffer_length); + break; + } + + case MYSQL_TYPE_SHORT: + { + req->inBind[i].is_unsigned = FALSE; +//d("SHORT %d %s %d", i, fld->name, req->inBind[i].buffer_length); + break; + } + + case MYSQL_TYPE_INT24: + { + req->inBind[i].is_unsigned = FALSE; +//d("INT24 %d %s %d", i, fld->name, req->inBind[i].buffer_length); + break; + } + + case MYSQL_TYPE_LONG: + { + req->inBind[i].is_unsigned = FALSE; +//d("LONG %d %s %d", i, fld->name, req->inBind[i].buffer_length); + break; + } + + case MYSQL_TYPE_LONGLONG: + { + req->inBind[i].is_unsigned = FALSE; +//d("LONGLONG %d %s %d", i, fld->name, req->inBind[i].buffer_length); + break; + } + + case MYSQL_TYPE_FLOAT: + { +//d("FLOAT %d %s %d", i, fld->name, req->inBind[i].buffer_length); + break; + } + + case MYSQL_TYPE_DOUBLE: + { +//d("DOUBLE %d %s %d", i, fld->name, req->inBind[i].buffer_length); + break; + } + + case MYSQL_TYPE_NEWDECIMAL: + { +//d("NEWDECIMAL %d %s %d", i, fld->name, req->inBind[i].buffer_length); + break; + } + + case MYSQL_TYPE_TIME: + case MYSQL_TYPE_DATE: + case MYSQL_TYPE_DATETIME: + case MYSQL_TYPE_TIMESTAMP: + { +//d("DATE / TIME ish %d %s %d", i, fld->name, req->inBind[i].buffer_length); + break; + } + + case MYSQL_TYPE_STRING: + case MYSQL_TYPE_VAR_STRING: + { +//d("STRING / VARSTRING %d %s %d", i, fld->name, req->inBind[i].buffer_length); + req->inBind[i].is_null = xzalloc(sizeof(my_bool)); + req->inBind[i].length = xzalloc(sizeof(unsigned long)); + break; + } + + case MYSQL_TYPE_TINY_BLOB: + case MYSQL_TYPE_BLOB: + case MYSQL_TYPE_MEDIUM_BLOB: + case MYSQL_TYPE_LONG_BLOB: + { +//d("BLOBs %d %s %d", i, fld->name, req->inBind[i].buffer_length); + req->inBind[i].is_null = xzalloc(sizeof(my_bool)); + break; + } + + case MYSQL_TYPE_BIT: + { + req->inBind[i].is_null = xzalloc(sizeof(my_bool)); +//d("BIT %d %s %d", i, fld->name, req->inBind[i].buffer_length); + break; + } + + case MYSQL_TYPE_NULL: + { +//d("NULL %d %s %d", i, fld->name, req->inBind[i].buffer_length); + break; + } + } + } + } + +// TODO - if this is not a count, setup result bind paramateres, may be needed for counts as well. + if (CT_SELECT == req->type) + { + prepare_meta_result = mysql_stmt_result_metadata(req->prep); + if (!prepare_meta_result) + { + E(" mysql_stmt_result_metadata() error %d, returned no meta information - %s\n", mysql_stmt_errno(req->prep), mysql_stmt_error(req->prep)); + ret++; + goto freeIt; + } + } + + if (count) + { +I("count!!!!!!!!!!!!!!!!"); + } + else if (CT_SELECT == req->type) + { + req->outCount = mysql_num_fields(prepare_meta_result); + i = 0; + while (req->outParams[i] != NULL) + i++; + if (0 == i) // Passing in {NULL} as req->outParams means "return all of them". + { + req->outParams = xzalloc((req->outCount + 1) * sizeof(char *)); + req->freeOutParams = TRUE; + qlisttbl_obj_t obj; + memset((void*)&obj, 0, sizeof(obj)); + req->fields->flds->lock(req->fields->flds); + while (req->fields->flds->getnext(req->fields->flds, &obj, NULL, false) == true) + { + dbField *fld = (dbField *) obj.data; + req->outParams[i] = fld->name; + i++; + } + req->outParams[i] = NULL; + req->fields->flds->unlock(req->fields->flds); + } + if (i != req->outCount) + { + E("Out parameters count doesn't match %d != %d foqr - %s", i, req->outCount, req->sql); + ret++; + goto freeIt; + } + req->outBind = xzalloc(i * sizeof(MYSQL_BIND)); +//W("Allocated %d %d outBinds for %s", i, req->outCount, req->sql); + for (i = 0; i < req->outCount; i++) + { + dbField *fld = req->fields->flds->get(req->fields->flds, req->outParams[i], NULL, false); + + if (NULL == fld) + { + E("Unknown output field %d %s.%s foqr - %s", i, req->table, req->outParams[i], req->sql); + ret++; + goto freeIt; + } + else + { + // https://blog.cotten.io/a-taste-of-mysql-in-c-87c5de84a31d?gi=ab3dd1425b29 + // For some gotchas about all of this binding bit. + req->outBind[i].buffer_type = fld->type; + req->outBind[i].buffer = xzalloc(fld->length + 1); // Note the + 1 is for string types, and a waste for the rest. + req->outBind[i].buffer_length = fld->length + 1; + req->outBind[i].error = xzalloc(sizeof(my_bool)); + req->outBind[i].is_null = xzalloc(sizeof(my_bool)); + switch(fld->type) + { + case MYSQL_TYPE_TINY: + { +//d("TINY %d %s %d", i, fld->name, req->outBind[i].buffer_length); + break; + } + + case MYSQL_TYPE_SHORT: + { +//d("SHORT %s %d", fld->name, req->outBind[i].buffer_length); + req->outBind[i].is_unsigned = FALSE; + break; + } + + case MYSQL_TYPE_INT24: + { +//d("INT24 %s %d", fld->name, req->outBind[i].buffer_length); + req->outBind[i].is_unsigned = FALSE; + break; + } + + case MYSQL_TYPE_LONG: + { +//d("LONG %d %s %d", i, fld->name, req->outBind[i].buffer_length); + req->outBind[i].is_unsigned = FALSE; + break; + } + + case MYSQL_TYPE_LONGLONG: + { +//d("LONGLONG %s %d", fld->name, req->outBind[i].buffer_length); + req->outBind[i].is_unsigned = FALSE; + break; + } + + case MYSQL_TYPE_FLOAT: + { +//d("FLOAT %s %d", fld->name, req->outBind[i].buffer_length); + break; + } + + case MYSQL_TYPE_DOUBLE: + { +//d("DOUBLE %s %d", fld->name, req->outBind[i].buffer_length); + break; + } + + case MYSQL_TYPE_NEWDECIMAL: + { +//d("NEWDECIMAL %s %d", fld->name, req->outBind[i].buffer_length); + break; + } + + case MYSQL_TYPE_TIME: + case MYSQL_TYPE_DATE: + case MYSQL_TYPE_DATETIME: + case MYSQL_TYPE_TIMESTAMP: + { +//d("DATE / TIME ish %s %d", fld->name, req->outBind[i].buffer_length); + break; + } + + case MYSQL_TYPE_STRING: + case MYSQL_TYPE_VAR_STRING: + { +//d("STRING / VARSTRING %s %d", fld->name, req->outBind[i].buffer_length); + req->outBind[i].length = xzalloc(sizeof(unsigned long)); + break; + } + + case MYSQL_TYPE_TINY_BLOB: + case MYSQL_TYPE_BLOB: + case MYSQL_TYPE_MEDIUM_BLOB: + case MYSQL_TYPE_LONG_BLOB: + { +//d("BLOBs %s %d", fld->name, req->outBind[i].buffer_length); + break; + } + + case MYSQL_TYPE_BIT: + { +//d("BIT %s %d", fld->name, req->outBind[i].buffer_length); + break; + } + + case MYSQL_TYPE_NULL: + { +//d("NULL %s %d", fld->name, req->outBind[i].buffer_length); + break; + } + } + } + } + if (mysql_stmt_bind_result(req->prep, req->outBind)) + { + E("Bind failed error %d.", mysql_stmt_errno(req->prep)); + ret++; + goto freeIt; + } + } + } + + +//d("input bind for %s", req->sql); + for (i = 0; i < req->inCount; i++) + { + dbField *fld = req->fields->flds->get(req->fields->flds, req->inParams[i], NULL, false); + + if (NULL == fld) + { + E("Unknown input field %s.%s for - %s", req->table, req->inParams[i], req->sql); + ret++; + goto freeIt; + } + else + { + switch(fld->type) + { + case MYSQL_TYPE_TINY: + { + int c = va_arg(ap, int); + signed char d = (signed char) c; + + memcpy(req->inBind[i].buffer, &d, (size_t) fld->length); +//T("TINY %d %s %d", i, fld->name, req->inBind[i].buffer_length); + break; + } + + case MYSQL_TYPE_SHORT: + { + int c = va_arg(ap, int); + short int d = (short int) c; + + memcpy(req->inBind[i].buffer, &d, (size_t) fld->length); +//T("SHORT %d %s %d = %d", i, fld->name, req->inBind[i].buffer_length, c); + break; + } + + case MYSQL_TYPE_INT24: + { + int d = va_arg(ap, int); + + memcpy(req->inBind[i].buffer, &d, (size_t) fld->length); +//T("INT24 %d %s %d - %d", i, fld->name, req->inBind[i].buffer_length, d); + break; + } + + case MYSQL_TYPE_LONG: + { + long d = va_arg(ap, long); + + memcpy(req->inBind[i].buffer, &d, (size_t) fld->length); +//T("LONG %d %s %d = %ld", i, fld->name, req->inBind[i].buffer_length, d); + break; + } + + case MYSQL_TYPE_LONGLONG: + { + long long int d = va_arg(ap, long long int); + + memcpy(req->inBind[i].buffer, &d, (size_t) fld->length); +//T("LONGLONG %d %s %d = %lld", i, fld->name, req->inBind[i].buffer_length, d); + break; + } + + case MYSQL_TYPE_FLOAT: + { + double c = va_arg(ap, double); + float d = (float) c; + + memcpy(req->inBind[i].buffer, &d, (size_t) fld->length); +//T("FLOAT %d %s %d = %f", i, fld->name, req->inBind[i].buffer_length, d); + break; + } + + case MYSQL_TYPE_DOUBLE: + { + double d = va_arg(ap, double); + + memcpy(req->inBind[i].buffer, &d, (size_t) fld->length); +//T("DOUBLE %d %s %d = %f", i, fld->name, req->inBind[i].buffer_length, d); + break; + } + + case MYSQL_TYPE_NEWDECIMAL: + { +//T("NEWDECIMAL %d %s %d", i, fld->name, req->inBind[i].buffer_length); + break; + } + + case MYSQL_TYPE_TIME: + case MYSQL_TYPE_DATE: + case MYSQL_TYPE_DATETIME: + case MYSQL_TYPE_TIMESTAMP: + { + MYSQL_TIME d = va_arg(ap, MYSQL_TIME); + + memcpy(req->inBind[i].buffer, &d, (size_t) fld->length); +//T("DATE / TIME ish %d %s %d", i, fld->name, req->inBind[i].buffer_length); + break; + } + + case MYSQL_TYPE_STRING: + case MYSQL_TYPE_VAR_STRING: + { + char *d = va_arg(ap, char *); + unsigned long l = strlen(d); + + if (l > fld->length) + l = fld->length; + *(req->inBind[i].length) = l; + strncpy(req->inBind[i].buffer, d, (size_t) l); + ((char *) req->inBind[i].buffer)[l] = '\0'; +//T("STRING / VARSTRING %d %s %d = %s", i, fld->name, req->inBind[i].buffer_length, d); + break; + } + + case MYSQL_TYPE_TINY_BLOB: + case MYSQL_TYPE_BLOB: + case MYSQL_TYPE_MEDIUM_BLOB: + case MYSQL_TYPE_LONG_BLOB: + { +// 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. +//T("BLOBs %d %s %d", i, fld->name, req->inBind[i].buffer_length); + break; + } + + case MYSQL_TYPE_BIT: + { +//T("BIT %d %s %d", i, fld->name, req->inBind[i].buffer_length); + break; + } + + case MYSQL_TYPE_NULL: + { +//T("NULL %d %s %d", i, fld->name, req->inBind[i].buffer_length); + break; + } + } + } + } + if (mysql_stmt_bind_param(req->prep, req->inBind)) + { + E("Bind failed error %d.", mysql_stmt_errno(req->prep)); + ret++; + goto freeIt; + } + + +//d("Execute %s", req->sql); + + // do the prepared statement req->prep. + if (mysql_stmt_execute(req->prep)) + { + if (dbStmtCheckError(req, "Statement failed 0", req->sql)) + { + ret++; + goto freeIt; + } + } + + int fs = mysql_stmt_field_count(req->prep); + // stuff results back into req. + if (NULL != req->outBind) + { + req->rows = xmalloc(sizeof(rowData)); + req->rows->fieldNames = xzalloc(fs * sizeof(char *)); + if (mysql_stmt_store_result(req->prep)) + { + E(" mysql_stmt_store_result() failed %d: %s", mysql_stmt_errno(req->prep), mysql_stmt_error(req->prep)); + ret++; + goto freeIt; + } + req->rowCount = mysql_stmt_num_rows(req->prep); + if (0 == req->rowCount) + D("No rows returned from : %s\n", req->sql); + else + D("%d rows of %d fields returned from : %s\n", req->rowCount, fs, req->sql); + + req->rows->rows = qlist(0); + while (MYSQL_NO_DATA != mysql_stmt_fetch(req->prep)) + { + qhashtbl_t *flds = qhashtbl(0, 0); + + for (i = 0; i < req->outCount; i++) + { + dbField *fld = req->fields->flds->get(req->fields->flds, req->outParams[i], NULL, false); + + req->rows->fieldNames[i] = fld->name; + if (!*(req->outBind[i].is_null)) + { +//d("2.8 %s", req->rows->fieldNames[i]); + flds->put(flds, req->rows->fieldNames[i], req->outBind[i].buffer, req->outBind[i].buffer_length); + + switch(fld->type) + { + case MYSQL_TYPE_TINY: + { + break; + } + + case MYSQL_TYPE_SHORT: + { + char *t = xmprintf("%d", (int) *((int *) req->outBind[i].buffer)); + flds->putstr(flds, req->rows->fieldNames[i], t); + free(t); + break; + } + + case MYSQL_TYPE_INT24: + { + char *t = xmprintf("%d", (int) *((int *) req->outBind[i].buffer)); + flds->putstr(flds, req->rows->fieldNames[i], t); + free(t); + break; + } + + case MYSQL_TYPE_LONG: + { + if (NULL == req->outBind[i].buffer) + { + E("Field %d %s is NULL", i, fld->name); + ret++; + goto freeIt; + } + char *t = xmprintf("%d", (int) *((int *) (req->outBind[i].buffer))); +//d("Setting %i %s %s", i, fld->name, t); + flds->putstr(flds, req->rows->fieldNames[i], t); + free(t); + break; + } + + case MYSQL_TYPE_LONGLONG: + { + char *t = xmprintf("%d", (int) *((int *) req->outBind[i].buffer)); + flds->putstr(flds, req->rows->fieldNames[i], t); + free(t); + break; + } + + case MYSQL_TYPE_FLOAT: + { + break; + } + + case MYSQL_TYPE_DOUBLE: + { + break; + } + + case MYSQL_TYPE_NEWDECIMAL: + { + break; + } + + case MYSQL_TYPE_TIME: + case MYSQL_TYPE_DATE: + case MYSQL_TYPE_DATETIME: + case MYSQL_TYPE_TIMESTAMP: + { + break; + } + + case MYSQL_TYPE_STRING: + case MYSQL_TYPE_VAR_STRING: + { + break; + } + + case MYSQL_TYPE_TINY_BLOB: + case MYSQL_TYPE_BLOB: + case MYSQL_TYPE_MEDIUM_BLOB: + case MYSQL_TYPE_LONG_BLOB: + { + break; + } + + case MYSQL_TYPE_BIT: + { + break; + } + + case MYSQL_TYPE_NULL: + { + break; + } + } + } + else + D("Not setting data %s, coz it's NULL", fld->name); + } + req->rows->rows->addlast(req->rows->rows, flds, sizeof(qhashtbl_t)); + free(flds); + } + } + +freeIt: + if (prepare_meta_result) + mysql_free_result(prepare_meta_result); + if (mysql_stmt_free_result(req->prep)) + { + E("Statement result freeing failed %d: %s\n", mysql_stmt_errno(req->prep), mysql_stmt_error(req->prep)); + ret++; + } + +end: + va_end(ap); + + if (-1 == clock_gettime(CLOCK_REALTIME, &dbLast)) + perror_msg("Unable to get the time."); + n = (dbLast.tv_sec * 1000000000.0) + dbLast.tv_nsec; + T("dbDoSomething(%s) took %lf seconds", req->sql, (n - t) / 1000000000.0); + + return ret; +} + +// Copy the SQL results into the request structure. +void dbPull(reqData *Rd, char *table, rowData *rows) +{ + char *where; + qhashtbl_t *me = rows->rows->popfirst(rows->rows, NULL); + qhashtbl_obj_t obj; + + if (NULL != me) + { + memset((void*)&obj, 0, sizeof(obj)); + me->lock(me); + while(me->getnext(me, &obj, false) == true) + { + where = xmprintf("%s.%s", table, obj.name); +d("dbPull(Rd->database) %s = %s", where, (char *) obj.data); + Rd->database->putstr(Rd->database, where, (char *) obj.data); + free(where); + } + me->unlock(me); + me->free(me); + } + free(rows->fieldNames); + rows->rows->free(rows->rows); + free(rows); +} + +my_ulonglong dbCount(char *table, char *where) +{ + my_ulonglong ret = 0; + char *sql; + struct timespec now, then; + + if (-1 == clock_gettime(CLOCK_REALTIME, &then)) + perror_msg("Unable to get the time."); + + if (where) + sql = xmprintf("SELECT Count(*) FROM %s WHERE %s", table, where); + else + sql = xmprintf("SELECT Count(*) FROM %s", table); + + if (mysql_query(database, sql)) + { +// E("MariaDB error %d - Query failed 1: %s", mysql_errno(database), mysql_error(database)); + if (dbCheckError("Query failed 1", sql)) + { + ret = dbCount(table, where); + free(sql); + return ret; + } + } + else + { + MYSQL_RES *result = mysql_store_result(database); + + if (!result) + E("Couldn't get results set from %s\n: %s", sql, mysql_error(database)); + else + { + MYSQL_ROW row = mysql_fetch_row(result); + if (!row) + E("MariaDB error %d - Couldn't get row from %s\n: %s", mysql_errno(database), sql, mysql_error(database)); + else + ret = atoll(row[0]); + mysql_free_result(result); + } + } + + if (-1 == clock_gettime(CLOCK_REALTIME, &now)) + perror_msg("Unable to get the time."); + double n = (now.tv_sec * 1000000000.0) + now.tv_nsec; + double t = (then.tv_sec * 1000000000.0) + then.tv_nsec; +// T("dbCount(%s) took %lf seconds", sql, (n - t) / 1000000000.0); + free(sql); + return ret; +} + +my_ulonglong dbCountJoin(char *table, char *select, char *join, char *where) +{ + my_ulonglong ret = 0; + char *sql; + struct timespec now, then; + + if (-1 == clock_gettime(CLOCK_REALTIME, &then)) + perror_msg("Unable to get the time."); + + if (NULL == select) + select = "*"; + if (NULL == join) + join = ""; + + if (where) + sql = xmprintf("SELECT %s FROM %s %s WHERE %s", select, table, join, where); + else + sql = xmprintf("SELECT %s FROM %s", select, table, join); + + if (mysql_query(database, sql)) + { +// E("MariaDB error %d - Query failed 2: %s", mysql_errno(database), mysql_error(database)); + if (dbCheckError("Query failed 2", sql)) + { + ret = dbCountJoin(table, select, join, where); + free(sql); + return ret; + } + } + else + { + MYSQL_RES *result = mysql_store_result(database); + + if (!result) + E("MariaDB error %d - Couldn't get results set from %s\n: %s", mysql_errno(database), sql, mysql_error(database)); + else + ret = mysql_num_rows(result); + mysql_free_result(result); + } + + if (-1 == clock_gettime(CLOCK_REALTIME, &now)) + perror_msg("Unable to get the time."); + double n = (now.tv_sec * 1000000000.0) + now.tv_nsec; + double t = (then.tv_sec * 1000000000.0) + then.tv_nsec; +// T("dbCointJoin(%s) took %lf seconds", sql, (n - t) / 1000000000.0); + free(sql); + return ret; +} + + +void replaceStr(qhashtbl_t *ssi, char *key, char *value) +{ + ssi->putstr(ssi, key, value); +} + +void replaceLong(qhashtbl_t *ssi, char *key, my_ulonglong value) +{ + char *tmp = xmprintf("%lu", value); + + replaceStr(ssi, key, tmp); + free(tmp); +} + + +float timeDiff(struct timeval *now, struct timeval *then) +{ + if (0 == gettimeofday(now, NULL)) + { + struct timeval thisTime = { 0, 0 }; + double result = 0.0; + + thisTime.tv_sec = now->tv_sec; + thisTime.tv_usec = now->tv_usec; + if (thisTime.tv_usec < then->tv_usec) + { + thisTime.tv_sec--; + thisTime.tv_usec += 1000000; + } + thisTime.tv_usec -= then->tv_usec; + thisTime.tv_sec -= then->tv_sec; + result = ((double) thisTime.tv_usec) / ((double) 1000000.0); + result += thisTime.tv_sec; + return result; + } + + return 0.0; +} + + +gridStats *getStats(MYSQL *db, gridStats *stats) +{ + if (NULL == stats) + { + stats = xmalloc(sizeof(gridStats)); + stats->next = 30; + gettimeofday(&(stats->last), NULL); + stats->stats = qhashtbl(0, 0); + stats->stats->putstr(stats->stats, "version", "SledjChisl FCGI Dev 0.1"); + stats->stats->putstr(stats->stats, "grid", "my grid"); + stats->stats->putstr(stats->stats, "uri", "http://localhost:8002/"); + if (checkSimIsRunning("ROBUST")) + stats->stats->putstr(stats->stats, "gridOnline", "online"); + else + stats->stats->putstr(stats->stats, "gridOnline", "offline"); + } + else + { + static struct timeval thisTime; + if (stats->next > timeDiff(&thisTime, &(stats->last))) + return stats; + } + + I("Getting fresh grid stats."); + if (checkSimIsRunning("ROBUST")) + replaceStr(stats->stats, "gridOnline", "online"); + else + replaceStr(stats->stats, "gridOnline", "offline"); + + char *tmp; + my_ulonglong locIn = dbCount("Presence", "RegionID != '00000000-0000-0000-0000-000000000000'"); // Locals online but not HGing, and HGers in world. + my_ulonglong HGin = dbCount("Presence", "UserID NOT IN (SELECT PrincipalID FROM UserAccounts)"); // HGers in world. + + // Collect stats about members. + replaceLong(stats->stats, "hgers", HGin); + if (locIn >= HGin) // Does OpenSim have too many ghosts? + replaceLong(stats->stats, "inworld", locIn - HGin); + else + replaceLong(stats->stats, "inworld", 0); + tmp = xmprintf("GridExternalName != '%s'", stats->stats->getstr(stats->stats, "uri", false)); + replaceLong(stats->stats, "outworld", dbCount("hg_traveling_data", tmp)); + free(tmp); + replaceLong(stats->stats, "members", dbCount("UserAccounts", NULL)); + + // Count local and HG visitors for the last 30 and 60 days. + locIn = dbCountJoin("GridUser", "GridUser.UserID", "INNER JOIN UserAccounts ON GridUser.UserID = UserAccounts.PrincipalID", + "Login > UNIX_TIMESTAMP(FROM_UNIXTIME(UNIX_TIMESTAMP(now()) - 2419200))"); + HGin = dbCount("GridUser", "Login > UNIX_TIMESTAMP(FROM_UNIXTIME(UNIX_TIMESTAMP(now()) - 2419200))"); + replaceLong(stats->stats, "locDay30", locIn); + replaceLong(stats->stats, "day30", HGin); + replaceLong(stats->stats, "HGday30", HGin - locIn); + + locIn = dbCountJoin("GridUser", "GridUser.UserID", "INNER JOIN UserAccounts ON GridUser.UserID = UserAccounts.PrincipalID", + "Login > UNIX_TIMESTAMP(FROM_UNIXTIME(UNIX_TIMESTAMP(now()) - 4838400))"); + HGin = dbCount("GridUser", "Login > UNIX_TIMESTAMP(FROM_UNIXTIME(UNIX_TIMESTAMP(now()) - 4838400))"); + replaceLong(stats->stats, "locDay60", locIn); + replaceLong(stats->stats, "day60", HGin); + replaceLong(stats->stats, "HGday60", HGin - locIn); + + // Collect stats about sims. + replaceLong(stats->stats, "sims", dbCount("regions", NULL)); + replaceLong(stats->stats, "onlineSims", dbCount("regions", "sizeX != 0")); + replaceLong(stats->stats, "varRegions", dbCount("regions", "sizeX > 256 or sizeY > 256")); + replaceLong(stats->stats, "singleSims", dbCount("regions", "sizeX = 256 and sizeY = 256")); + replaceLong(stats->stats, "offlineSims", dbCount("regions", "sizeX = 0")); + + // Calculate total size of all regions. + my_ulonglong simSize = 0; + static dbRequest *rgnSizes = NULL; + if (NULL == rgnSizes) + { + static char *szi[] = {NULL}; + static char *szo[] = {"sizeX", "sizeY", NULL}; + rgnSizes = xzalloc(sizeof(dbRequest)); + rgnSizes->table = "regions"; + rgnSizes->inParams = szi; + rgnSizes->outParams = szo; + rgnSizes->where = "sizeX != 0"; + dbRequests->addfirst(dbRequests, &rgnSizes, sizeof(dbRequest *)); + } + dbDoSomething(rgnSizes, FALSE); // LEAKY + rowData *rows = rgnSizes->rows; + + qhashtbl_t *row; + while (NULL != (row = rows->rows->getat(rows->rows, 0, NULL, true))) + { + my_ulonglong x = 0, y = 0; + + tmp = row->getstr(row, "sizeX", false); + if (NULL == tmp) + E("No regions.sizeX!"); + else + x = atoll(tmp); + tmp = row->getstr(row, "sizeY", false); + if (NULL == tmp) + E("No regions.sizeY!"); + else + y = atoll(tmp); + simSize += x * y; + row->free(row); + rows->rows->removefirst(rows->rows); + } + free(rows->fieldNames); + rows->rows->free(rows->rows); + free(rows); + + tmp = xmprintf("%lu", simSize); + stats->stats->putstr(stats->stats, "simsSize", tmp); + free(tmp); + gettimeofday(&(stats->last), NULL); + + return stats; +} + + +qhashtbl_t *toknize(char *text, char *delims) +{ + qhashtbl_t *ret = qhashtbl(0, 0); + + if (NULL == text) + return ret; + + char *txt = xstrdup(text), *token, dlm = ' ', *key, *val = NULL; + int offset = 0; + + while((token = qstrtok(txt, delims, &dlm, &offset)) != NULL) + { + if (delims[0] == dlm) + { + key = token; + val = &txt[offset]; + } + else if (delims[1] == dlm) + { + ret->putstr(ret, qstrtrim_head(key), token); +d(" %s = %s", qstrtrim_head(key), val); + val = NULL; + } + } + if (NULL != val) +{ + ret->putstr(ret, qstrtrim_head(key), val); +d(" %s = %s", qstrtrim_head(key), val); +} + free(txt); + return ret; +} + +void santize(qhashtbl_t *tbl) +{ + qhashtbl_obj_t obj; + + memset((void*)&obj, 0, sizeof(obj)); + tbl->lock(tbl); + while(tbl->getnext(tbl, &obj, false) == true) + { + char *n = obj.name, *o = (char *) obj.data; + + qurl_decode(o); + tbl->putstr(tbl, n, o); + } + tbl->unlock(tbl); +} + +void outize(qgrow_t *reply, qhashtbl_t *tbl, char *label) +{ + reply->addstrf(reply, "%s:
\n\n", label); + qhashtbl_obj_t obj; + memset((void*)&obj, 0, sizeof(obj)); + tbl->lock(tbl); + while(tbl->getnext(tbl, &obj, false) == true) + reply->addstrf(reply, " %s = %s\n", obj.name, (char *) obj.data); + tbl->unlock(tbl); + reply->addstr(reply, "\n"); +} + +// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie +enum cookieSame +{ + CS_NOT, + CS_STRICT, + CS_LAX, // Apparently the default set by browsers these days. + CS_NONE +}; +typedef struct _cookie cookie; +struct _cookie +{ + char *value, *domain, *path; + // char *expires; // Use maxAge instead, it's far simpler to figure out. + int maxAge; + boolean secure, httpOnly; + enum cookieSame site; +}; + +void freeCookie(reqData *Rd, char *cki) +{ + cookie *ck0 = Rd->Rcookies->get(Rd->Rcookies, cki, NULL, false); + + if (NULL != ck0) + { + if (NULL != ck0->value) + free(ck0->value); + Rd->Rcookies->remove(Rd->Rcookies, cki); + } +} + +cookie *setCookie(reqData *Rd, char *cki, char *value) +{ + cookie *ret = xzalloc(sizeof(cookie)); + char *cook = xstrdup(cki); + int l, i; + +// TODO - would URL encoding do the trick? + // Validate this, as there is a limited set of characters allowed. + qstrreplace("tr", cook, "()<>@,;:\\\"/[]?={} \t", "_"); + freeCookie(Rd, cook); + l = strlen(cook); + for (i = 0; i < l; i++) + { + if (iscntrl(cook[i]) != 0) + cook[i] = '_'; + } + l = strlen(value); + if (0 != l) + ret->value = qurl_encode(value, l); + else + ret->value = xstrdup(""); + ret->httpOnly = TRUE; + ret->site = CS_STRICT; + ret->secure = TRUE; + ret->path = getStrH(Rd->headers, "SCRIPT_NAME"); + Rd->Rcookies->put(Rd->Rcookies, cook, ret, sizeof(cookie)); + free(ret); + ret = Rd->Rcookies->get(Rd->Rcookies, cook, NULL, false); + free(cook); + return ret; +} + +char *getCookie(qhashtbl_t *cookies, char *cki) +{ + char *ret = NULL; + cookie *ck = (cookie *) cookies->get(cookies, cki, NULL, false); + + if (NULL != ck) + ret = ck->value; + return ret; +} + +void outizeCookie(qgrow_t *reply, qhashtbl_t *tbl, char *label) +{ + reply->addstrf(reply, "%s:
\n\n", label); + qhashtbl_obj_t obj; + memset((void*)&obj, 0, sizeof(obj)); + tbl->lock(tbl); + while(tbl->getnext(tbl, &obj, false) == true) + reply->addstrf(reply, " %s = %s\n", obj.name, ((cookie *) obj.data)->value); + tbl->unlock(tbl); + reply->addstr(reply, "\n"); +} + +void list2cookie(reqData *Rd, char *cki, qlist_t *list) +{ + char *t0 = xstrdup(""); + qlist_obj_t obj; + + memset((void*)&obj, 0, sizeof(obj)); // must be cleared before call + list->lock(list); + while (list->getnext(list, &obj, false) == true) + { + char *t1 = xmprintf("%s\n%s", t0, (char *) obj.data); + + free(t0); + t0 = t1; + } + list->unlock(list); + setCookie(Rd, cki, &t0[1]); // Skip the first empty one. +// TODO - should set a very short maxAge. + free(t0); +} + +qlist_t *cookie2list(qhashtbl_t *cookies, char *cki) +{ + qlist_t *ret = NULL; + cookie *ck = (cookie *) cookies->get(cookies, cki, NULL, false); + + if (NULL != ck) + { + if (NULL != ck->value) + { + qurl_decode(ck->value); + ret = qstrtokenizer(ck->value, "\n"); + free(ck->value); + } +// TODO - should send the "delete this cookie" thing to the browser. + cookies->remove(cookies, cki); + } + return ret; +} + + +enum fragmentType +{ + FT_TEXT, + FT_PARAM, + FT_LUA +}; + +typedef struct _fragment fragment; +struct _fragment +{ + enum fragmentType type; + int length; + char *text; +}; + +static void HTMLdebug(qgrow_t *reply) +{ + reply->addstrf(reply, + "\n" + "
\n" + "\n" + " \n" + ); +} + +static void HTMLheader(qgrow_t *reply, char *title) +{ + reply->addstrf(reply, + "\n" + " \n" + "DEBUG
\n" + "\n" + "\n" + "DEBUG log
\n" + " \n" + "%s \n" + " \n" + " \n" + , title); + reply->addstrf(reply, " \n"); + + if (DEBUG) + reply->addstrf(reply, " \n"); + + reply->addstrf(reply, + " \n" + " \n" + " \n" + " \n" + ); + reply->addstrf(reply, "\n"); + if (DEBUG) + HTMLdebug(reply); +} + +// TODO - maybe escape non printables as well? +char *HTMLentities[] = +{ + "", "", "", "", "", "", "", "", "", // NUL SOH STX ETX EOT ENQ ACK BEL BS + "	", "
", + "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", // VT FF CR SO SI DLE DC1 DC2 DC3 DC4 NAK SYN ETB CAN EM SUB ESC FS GS RS US + " ", // Space + "!", """, + "#", "$", + "%", "&", + "'", + "(", ")", + "*", + "+", + ",", + "-", + ".", + "/", + "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", + ":", ";", + "<", "=", ">", + "?", + "@", + "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", + "[", "\", "]", + "^", + "_", + "`", + "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", + "{", "|", "}", + "~", + "", // DEL + " " // This is actually 160, not 128, but I hack around that. +}; +static void HTMLescapeString(qgrow_t *reply, char *string) +{ + size_t l = strlen(string); + char *t = xmalloc(l * 10 + 1); + int i, j = 0; + boolean space = FALSE; + + for (i = 0; i < l; i++) + { + int s = string[i]; + + // Alternate long line of spaces with space and . + if (' ' == s) + { + if (space) + { + s = 128; + space = FALSE; + } + else + space = TRUE; + } + else + { + space = FALSE; + if (128 == s) // The real 128 character. + { + t[j++] = ' '; + continue; + } + } + + if (128 >= s) + { + char *r = HTMLentities[s]; + size_t m = strlen(r); + int k; + + for (k = 0; k < m; k++) + t[j++] = r[k]; + } + else + t[j++] = ' '; + } + t[j] = '\0'; + reply->addstr(reply, t); + free(t); +} + +static void HTMLtable(qgrow_t *reply, MYSQL *db, MYSQL_RES *result, char *caption, char *URL, char *id) +{ + char *tbl = ""; + char *address, *addrend, *t, *t0; + int count = 0, c = -1, i; + MYSQL_ROW row; + MYSQL_FIELD *fields = mysql_fetch_fields(result); + + reply->addstrf(reply, "\n"); + reply->addstr(reply, + ""); + mysql_free_result(result); +} + +static void HTMLhidden(qgrow_t *reply, char *name, char *val) +{ + if ((NULL != val) && ("" != val)) + { + reply->addstrf(reply, " addstr(reply, "\">\n"); + } +} + +static void HTMLform(qgrow_t *reply, char *action, char *token) +{ + reply->addstrf(reply, " \n"); +} + +static void HTMLcheckBox(qgrow_t *reply, char *name, char *title, boolean checked, boolean required) +{ + // HTML is an absolute fucking horror. This is so that we got an off sent to us if the checkbox is off, otherwise we get nothing. + HTMLhidden(reply, name, "off"); + reply->addstrf(reply, "
%s \n", caption); + + if (!fields) + E("Failed fetching fields: %s", mysql_error(db)); + while ((row = mysql_fetch_row(result))) + { + reply->addstr(reply, ""); + address = xmprintf(""); + addrend = ""; + + if (-1 == c) + c = mysql_num_fields(result); + + if (0 == count) + { + for (i = 0; i < c; i++) + { + char *s = fields[i].name; + + reply->addstrf(reply, " \n%s ", s); + } + reply->addstr(reply, ""); + } + + if (NULL != URL) + { + free(address); + address = xmprintf("addstrf(reply, " \n"); + + free(address); + count++; + } + + reply->addstr(reply, "%s&%s=%s\">%s%s ", address, id, t0, t0, addrend); + else + reply->addstrf(reply, "%s ", t0); + } + } + reply->addstr(reply, ""); + reply->addstrf(reply, "", title); +// reply->addstrf(reply, "> %s ⛝ □ 🞐 🞎 🞎 ☐ ▣️ ◉ ○ ", title); + reply->addstrf(reply, "
\n"); +} + +static void HTMLtextArea(qgrow_t *reply, char *name, char *title, int rows, int cols, int min, int max, char *holder, char *comp, char *spell, char *wrap, char *val, boolean required, boolean readOnly) +{ + reply->addstrf(reply, " \n"); + } + else + reply->addstrf(reply, ">\n"); +} + +static void HTMLtext(qgrow_t *reply, char *type, char *title, char *name, char *val, int size, int max, boolean required) +{ + reply->addstrf(reply, " \n"); +} + +static void HTMLselect(qgrow_t *reply, char *title, char *name) +{ + if (NULL == title) + reply->addstrf(reply, " "); +} + +static void HTMLoption(qgrow_t *reply, char *title, boolean selected) +{ + char *sel = ""; + + if (selected) + sel = " selected"; + reply->addstrf(reply, " \n", title, sel, title); +} + +static void HTMLbutton(qgrow_t *reply, char *name, char *title) +{ + reply->addstrf(reply, " \n", name, title); +} + +static void HTMLlist(qgrow_t *reply, char *title, qlist_t *list) +{ + qlist_obj_t obj; + + reply->addstrf(reply, "%s\n", title); + memset((void*)&obj, 0, sizeof(obj)); // must be cleared before call + list->lock(list); + while (list->getnext(list, &obj, false) == true) + { + reply->addstr(reply, "
\n"); +} + +static int count = 0; +void HTMLfill(reqData *Rd, enum fragmentType type, char *text, int length) +{ + char *tmp; + + switch (type) + { + case FT_TEXT: + { + if (length) + Rd->reply->add(Rd->reply, (void *) text, length * sizeof(char)); + break; + } + + case FT_PARAM: + { + if (strcmp("DEBUG", text) == 0) + { + if (DEBUG) + { + Rd->reply->addstrf(Rd->reply, "- "); + HTMLescapeString(reply, (char *) obj.data); + reply->addstr(reply, "
\n"); + } + list->unlock(list); + reply->addstr(reply, "FastCGI SledjChisl
\n" + "Request number %d, Process ID: %d
\n", count++, getpid()); + Rd->reply->addstrf(Rd->reply, "libfcgi version: %s
\n", FCGI_VERSION); + Rd->reply->addstrf(Rd->reply, "Lua version: %s
\n", LUA_RELEASE); + Rd->reply->addstrf(Rd->reply, "LuaJIT version: %s
\n", LUAJIT_VERSION); + Rd->reply->addstrf(Rd->reply, "MySQL client version: %s
\n", mysql_get_client_info()); + outize(Rd->reply, Rd->headers, "Environment"); + outize(Rd->reply, Rd->cookies, "Cookies"); + outize(Rd->reply, Rd->queries, "Query"); + outize(Rd->reply, Rd->body, "POST body"); + outize(Rd->reply, Rd->stuff, "Stuff"); + showSesh(Rd->reply, &Rd->shs); + if (Rd->lnk) showSesh(Rd->reply, Rd->lnk); + outize(Rd->reply, Rd->database, "Database"); + outizeCookie(Rd->reply, Rd->Rcookies, "Reply Cookies"); + outize(Rd->reply, Rd->Rheaders, "Reply HEADERS"); + } + } + else if (strcmp("URL", text) == 0) + Rd->reply->addstrf(Rd->reply, "%s://%s%s", Rd->Scheme, Rd->Host, Rd->Script); + else + { + if ((tmp = Rd->stats->stats->getstr(Rd->stats->stats, text, false)) != NULL) + Rd->reply->addstr(Rd->reply, tmp); + else + Rd->reply->addstrf(Rd->reply, "%s", text); + } + break; + } + + case FT_LUA: + break; + } +} + +static void HTMLfooter(qgrow_t *reply) +{ + reply->addstrf(reply, "\n" + "\n"); +// reply->addstr(reply, "Experimental account manager
\n" + "This account manager system is currently experimental, and under heavy development. " + " Which means it's not all written yet, and things may break.
\n" + "To create an account, choose a name and password, type them in, click the 'create account' button.
" + "On the next page, fill in all your details, then click on the 'confirm' button.
" + "We follow the usual web site registration process, which sends a validation email, with a link to click. " + " When you get that email, click on the link, or copy it into a web browser.
" + "You will then be logged off. Now you have to wait for an admin to approve your new account. " + " They should check with the person you listed as vouching for you first. They will tell you after they approve your account.
" + "Missing bits that are still being written - editing accounts, listing accounts, deleting accounts.
\n" + "\n\n"); + reply->addstr(reply, +// "\n" +// "\n" + "\n" + " \n" + "\n" + " \n" + "\n\n"); +} + + +fragment *newFragment(enum fragmentType type, char *text, int len) +{ + fragment *frg = xmalloc(sizeof(fragment)); + + frg->type = type; + frg->length = len; + frg->text = xmalloc(len + 1); + memcpy(frg->text, text, len); + frg->text[len] = '\0'; + return frg; +} + +qlist_t *fragize(char *mm, size_t length) +{ + qlist_t *fragments = qlist(QLIST_THREADSAFE); + fragment *frg0, *frg1; + + char *h; + int i, j = 0, k = 0, l, m; + + // Scan for server side includes style markings. + for (i = 0; i < length; i++) + { + if (i + 5 < length) + { + if (('<' == mm[i]) && ('!' == mm[i + 1]) && ('-' == mm[i + 2]) && ('-' == mm[i + 3]) && ('#' == mm[i + 4])) // '' + i += 4; + } + frg0 = newFragment(FT_TEXT, &mm[k], m - k); + fragments->addlast(fragments, frg0, sizeof(fragment)); + fragments->addlast(fragments, frg1, sizeof(fragment)); + free(frg0); + free(frg1); + k = i; + break; + } + } + } + } + } + } + } + } + } + frg0 = newFragment(FT_TEXT, &mm[k], length - k); + fragments->addlast(fragments, frg0, sizeof(*frg0)); + free(frg0); + + return fragments; +} + +void unfragize(qlist_t *fragments, reqData *Rd, boolean fre) +{ + qlist_obj_t lobj; + memset((void *) &lobj, 0, sizeof(lobj)); + fragments->lock(fragments); + while (fragments->getnext(fragments, &lobj, false) == true) + { + fragment *frg = (fragment *) lobj.data; + if (NULL == frg->text) + { + E("NULL fragment!"); + continue; + } + if (NULL != Rd) + HTMLfill(Rd, frg->type, frg->text, frg->length); + if (fre) + free(frg->text); + } + fragments->unlock(fragments); + if (fre) + fragments->free(fragments); +} + +HTMLfile *checkHTMLcache(char *file) +{ + if (NULL == HTMLfileCache) + HTMLfileCache = qhashtbl(0, 0); + + HTMLfile *ret = (HTMLfile *) HTMLfileCache->get(HTMLfileCache, file, NULL, true); + int fd = open(file, O_RDONLY); + size_t length = 0; + + if (-1 == fd) + { + HTMLfileCache->remove(HTMLfileCache, file); + free(ret); + ret = NULL; + } + else + { + struct stat sb; + if (fstat(fd, &sb) == -1) + { + HTMLfileCache->remove(HTMLfileCache, file); + free(ret); + ret = NULL; + E("Failed to stat %s", file); + } + else + { + if ((NULL != ret) && (ret->last.tv_sec < sb.st_mtim.tv_sec)) + { + HTMLfileCache->remove(HTMLfileCache, file); + free(ret); + ret = NULL; + } + + if (NULL == ret) + { + char *mm = MAP_FAILED; + + ret = xmalloc(sizeof(HTMLfile)); + length = sb.st_size; + ret->last.tv_sec = sb.st_mtim.tv_sec; + ret->last.tv_nsec = sb.st_mtim.tv_nsec; + + D("Loading web template %s, which is %d bytes long.", file, length); + mm = mmap(NULL, length, PROT_READ, MAP_SHARED | MAP_POPULATE, fd, 0); + if (mm == MAP_FAILED) + { + HTMLfileCache->remove(HTMLfileCache, file); + free(ret); + ret = NULL; + E("Failed to mmap %s", file); + } + else + { + ret->fragments = fragize(mm, length); + if (-1 == munmap(mm, length)) + FCGI_fprintf(FCGI_stderr, "Failed to munmap %s\n", file); + + HTMLfileCache->put(HTMLfileCache, file, ret, sizeof(*ret)); + } + } + close(fd); + } + } + + return ret; +} + + + + + +/* TODO - + + On new user / password reset. +. Both should have all the same security concerns as the login page, they are basically logins. +. Codes should be "very long", "(for example, 16 case-sensitive alphanumeric characters)" +. "confirm" button hit on "accountCreationPage" or "resetPasswordPage" +. generate a new token, keep it around for idleTimeOut (or at least 24 hours), call it .linky instead of .lua +. hash the linky for the file name, for the same reason we hash the hashish with pepper for the leaf-node. +. Include user level field, new users get -200. +. Store the linky itself around somewhere we can find it quickly for logged in users. +. store it in the regenerated session +. Scratch that, we should never store the raw linky, see above about hashing the linky. + +. The linky is just like the session token, create it in exactly the same way. +. Linky is base64() of the binary, so it's short enough to be a file name, and not too long for the URL. +. But we only get to send one of them as a linky URL, no backup cookies / body / headers. +. Sooo, need to separate the session stuff out of Rd->stuff. +. Use two separate qhashtbl's, Rd->session and Rd->linky. + + For new user +. create their /opt/opensim_SC/var/lib/users/UUID.lua account record, and symlink firstName_lastName.lua to it. +. They can log on, + but all they can do is edit their email to send a new validation code, and enter the validation code. + They can reset their password. +. Warn them on login and any page refresh that there is an outstanding validation awaiting them. + For reset password +. Let them do things as normal, in case this was just someone being mean to them, coz their email addy might be public. +. Including the usual logging out and in again with their old password. +. Warn them on login and any page refresh that there is an outstanding password reset awaiting them. +. email linky, which is some or all of the token result bits strung together, BASE64 encode the result. +. regenerate the usual token +. user clicks on the linky (or just enters the linky in a field) +. validate the linky token. + compare the level field to the linky type in the linky URL, new users -200 would be "../validateUser/.." and not "../resetPassword/.." +. delete the linky token +. Particularly important for the forgotten password email, since now that token is in the wild, and is used to reset a password. + Which begs the question, other than being able to recieve the email, how do we tell it's them? + Security questions suck, too easily guessed. + Ask their DoB. Still sucky, coz "hey it's my birthday today" is way too leaky. + This is what Multi Factor Autentication is good for, and that's on the TODO list. + Also, admins should have a harder time doing password resets. + Must be approved by another admin? + Must log onto the server via other means to twiddle something there? + For password reset page + Ask their DoB to help confirm it's them. + validate the DoB, delete tokens and back to the login page if they get it wrong + Should warn people on the accountCreationPage that DoB might be used this way. + ask them for the new password, twice + Create a new passwordSalt and passwordHash, store them in the auth table. +. For validate new user page +. tell them they have validated +. create their OpenSim account UserAccounts.UserTitle and auth tables, not GridUser table +. create their GridUser record. +. update their UserAccounts.Userlevel and UserAccounts.UserTitle +. send them to the login page. +. regenerate the usual token +? let user stay logged on? + Check best practices for this. + + Check password strength. + https://stackoverflow.com/questions/549/the-definitive-guide-to-form-based-website-authentication?rq=1 + Has some pointers to resources in the top answers "PART V: Checking Password Strength" section. + + "PART VI: Much More - Or: Preventing Rapid-Fire Login Attempts" and "PART VII: Distributed Brute Force Attacks" is also good for - + Login attempt throttling. + Deal with dictionary attacks by slowing down access on password failures etc. + + Deal with editing yourself. + Deal with editing others, but only as god. + + + Regularly delete old session files and ancient newbies. + + + Salts should be "lengthy" (128 bytes suggested in 2007) and random. Should be per user to. Or use a per user and a global one, and store the global one very securely. + And store hash(salt+password). + On the other hand, the OpenSim / SL password hashing function may be set in concrete in all the viewers. I'll have to find out. + So far I have discovered - + On login server side if the password doesn't start with $1$, then password = "$1$" + Util.Md5Hash(passwd); + remove the "$1$ bit + string hashed = Util.Md5Hash(password + ":" + data.Data["passwordSalt"].ToString()); + if (data.Data["passwordHash"].ToString() == hashed) + passwordHash is char(32), and as implied above, doesn't include the $1$ bit. passwordSalt is also char(32) + Cool VL and Impy at least sends the $1$ md5 hashed version over the wire. + Modern viewers obfuscate the details buried deep in C++ OOP crap. + Sent via XMLRPC + MD5 is considered broken since 2013, possibly longer. + Otherwise use a slow hashing function. bcrypt? scrypt? Argon2? PBKDF2? + https://security.stackexchange.com/questions/211/how-to-securely-hash-passwords + Should include a code in the result that tells us which algorithm was used, so we can change the algorithm at a later date. /etc/passwd does this. + Which is what the $1$ bit currently used between server and client is sorta for. + ++ Would be good to have one more level of this Rd->database stuff, so we know the type of the thing. + While qhashtbl has API for putting strings, ints, and memory, it has none for finding out what type a stored thing is. + Once I have a structure, I add things like "level needed to edit it", "OpenSim db structure to Lua file mapping", and other fanciness. + Would also help with the timestamp being stored for the session, it prints out binary gunk in the DEBUG. + Starting to get into object oriented territory here. B-) + I'll have to do it eventually anyway. + object->tostring(object), and replace the big switch() statements in the existing db code with small functions. + That's why the qLibc stuff has that format, coz C doesn't understand the concept of passing "this" as the first argument. + https://stackoverflow.com/questions/351733/how-would-one-write-object-oriented-code-in-c + https://stackoverflow.com/questions/415452/object-orientation-in-c + http://ooc-coding.sourceforge.net/ + + +https://owasp.org/www-project-cheat-sheets/cheatsheets/Input_Validation_Cheat_Sheet.html#Email_Address_Validation +https://cheatsheetseries.owasp.org/ +https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html +https://owasp.org/www-project-cheat-sheets/cheatsheets/Authentication_Cheat_Sheet.html +https://softwareengineering.stackexchange.com/questions/46716/what-technical-details-should-a-programmer-of-a-web-application-consider-before +https://wiki.owasp.org/index.php/OWASP_Guide_Project +https://stackoverflow.com/questions/549/the-definitive-guide-to-form-based-website-authentication?rq=1 +https://owasp.org/www-community/xss-filter-evasion-cheatsheet + A list of example XSS things to try. +*/ + + + +/* Four choices for the token - (https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html) + https://en.wikipedia.org/wiki/Cross-site_request_forgery + Has some more info. + +Large random value generated by a secure method (getrandom(2)). + Keep it secret, put it in hidden fields, or custom HTTP header (requires JavaScript but more secure than hidden fields). + NOT cookies or GET. Don't log it. +Cryptographically sign a session ID and timestamp. + Timestamp is for session timeouts. + Keep it secret, put it in hidden fields, or custom HTTP header (requires JavaScript but more secure than hidden fields). + Needs a secret key server side. +A strong HMAC (SHA-256 or better) of a session ID and timestamp. + The above document seems to imply that a key is used for this, the openssl EVP functions don't mention any way of supplying this key. + https://en.wikipedia.org/wiki/HMAC says there is a key as well. + https://www.openssl.org/docs/man1.1.0/man3/HMAC.html HAH! They can have keys. OpenSSL's docs suck. + Token = HMAC(sessionID+timestamp)+timestamp (Yes, timestamp is used twice). + Keep it secret, put it in hidden fields, or custom HTTP header (requires JavaScript but more secure than hidden fields). + Needs a secret key server side. +Double cookie + Large random value generated by a secure method set as a cookie and hidden field. Check they match. + Optional - encrypt / salted hash it in another cookie / hidden field. ++ Also a resin (BASE64 session key in the query string). + Not such a good idea to have something in the query, coz that screws with bookmarks. + https://security.stackexchange.com/questions/59470/double-submit-cookies-vulnerabilities + Though so far all the pages I find saying this don't say flat out say "use headers instead", though they do say "use HSTS". + https://security.stackexchange.com/questions/220797/is-the-double-submit-cookie-pattern-still-effective ++ Includes a work around that I might already be doing. +TODO - think it through, is it really secure against session hijacking? +TODO - document why we redirect POST to GET, coz it's a pain in the arse, and we have to do things twice. + +SOOOOO - use double cookie + hidden field. + No headers, coz I need JavaScript to do that. + No hidden field when redirecting post POST to GET, coz GET doesn't get those. + pepper = long pass phrase or some such stored in .sledjChisl.conf.lua, which has to be protected dvs1/opensimsc/0640 as well as the database credentials. + salt = large random value generated by a secure method (getrandom(2)). + seshID = large random value generated by a secure method (getrandom(2)). + timeStamp = mtime of the leaf-node file, set to current time when we are creating the token. + sesh = seshID + timeStamp + munchie = HMAC(sesh) + timeStamp The token hidden field + toke_n_munchie = HMAC(UUID + munchie) The token cookie + hashish = HMACkey(toke_n_munchie, salt) Salted token cookie & linky query +? resin = BASE64(hashish) Base64 token cookie + leaf-node = HMACkey(hashish, pepper) Stored token file name + + Leaf-node.lua (mtime is timeStamp) + IP, UUID, salt, seshID, user name, passwordSalt, passwordHash (last two for OpenSim password protocol) + + The test - (validateSesh() below) + we get hashish and toke_n_munchie + HMACkey(hashish + pepper) -> leaf-node + read leaf-node.lua -> IP, UUID, salt, seshID + get it's mtime -> timeStamp + seshID + timeStamp -> sesh + HMAC(sesh) + timeStamp -> munchie + if we got munchie in the hidden field, compare it + toke_n_munchie == HMAC(UUID + munchie) + For linky it'll be - + HMAC(UUID + munchie) -> toke_n_munchie + hashish == HMACkey(toke_n_munchie + salt) ++ If it's too old according to mtime, delete it and logout. + +I should make it easy to change the HMAC() function. Less important for these short lived sessions, more important for the linky URLs, most important for stored password hashes. + Same for the pepper. + +The required JavaScript might be like https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#xmlhttprequest--native-javascript- + NOTE - they somehow fucked up that anchor tag. + +NOTE - storing a pepper on the same RAID array as everything else will be a problem when it comes time to replace one of the disks. + It might have failed badly enough that you can't wipe it, but an attacker can dumpster dive it, replace the broken bit (firmware board), and might get lucky. + Also is a problem with SSD and rust storing good data on bad sectors in the spare sector pool, wear levelling, etc. + +https://stackoverflow.com/questions/16891729/best-practices-salting-peppering-passwords +*/ + + +qlisttbl_t *accountLevels = NULL; + + +static void bitch(reqData *Rd, char *message, char *log) +{ + addStrL(Rd->errors, message); + E("%s %s %s - %s %s", getStrH(Rd->headers, "REMOTE_ADDR"), Rd->shs.UUID, Rd->shs.name, message, log); +} + +/* "A token cookie that references a non-existent session, its value should be replaced immediately to prevent session fixation." +https://owasp.org/www-community/attacks/Session_fixation + Which describes the problem, but offers no solution. + See https://stackoverflow.com/questions/549/the-definitive-guide-to-form-based-website-authentication?rq=1. +I think this means send a new cookie. + I clear out the cookies and send blank ones with -1 maxAge, so they should get deleted. +*/ +static void bitchSession(reqData *Rd, char *message, char *log) +{ + if ('\0' != message[0]) + addStrL(Rd->errors, message); + C("%s %s %s - %s %s", getStrH(Rd->headers, "REMOTE_ADDR"), Rd->shs.UUID, Rd->shs.name, message, log); + Rd->shs.status = SHS_BOGUS; +} + + +// The ancient, insecure since 2011, Second Life / OpenSim password hashing algorithm. +char *newSLOSsalt(reqData *Rd) +{ + char *salt = NULL; + unsigned char *md5hash = xzalloc(17); + char uuid[37]; + uuid_t binuuid; + + uuid_generate_random(binuuid); + uuid_unparse_lower(binuuid, uuid); + if (!qhashmd5((void *) uuid, strlen(uuid), md5hash)) + bitch(Rd, "Internal error.", "newSLOSsalt() - qhashmd5(new uuid) failed."); + else + salt = qhex_encode(md5hash, 16); + free(md5hash); + return salt; +} + +char *checkSLOSpassword(reqData *Rd, char *salt, char *password, char *passwordHash, char *fail) +{ + char *ret = NULL; + int rt = 0; + unsigned char *md5hash = xzalloc(17); + char *hash = NULL, *passHash = NULL; + +T("checkSLOSpassword(%s, %s, %s, ", password, salt, passwordHash, fail); + // Calculate passHash. + if (!qhashmd5((void *) password, strlen(password), md5hash)) + { + bitch(Rd, "Internal error.", "checkSLOSpassword() - qhashmd5(password) failed."); + rt++; + } + else + { + passHash = qhex_encode(md5hash, 16); + hash = xmprintf("%s:%s", passHash, salt); + if (!qhashmd5((void *) hash, strlen(hash), md5hash)) + { + bitch(Rd, "Internal error.", "checkSLOSpassword() - qhashmd5(password:salt) failed."); + rt++; + } + else + { + ret = qhex_encode(md5hash, 16); + } + free(hash); + free(passHash); + } + + // If one was passed in, compare it. + if ((NULL != ret) && (NULL != passwordHash) && (strcmp(ret, passwordHash) != 0)) + { + bitch(Rd, fail, "Password doesn't match passwordHash"); + E(" %s %s - %s != %s", password, salt, ret, passwordHash); + rt++; + free(ret); + ret = NULL; + } + free(md5hash); + + return ret; +} + + +int LuaToHash(reqData *Rd, char *file, char *var, qhashtbl_t *tnm, int ret, struct stat *st, struct timespec *now, char *type) +{ + struct timespec then; + + if (-1 == clock_gettime(CLOCK_REALTIME, &then)) + perror_msg("Unable to get the time."); + I("Reading %s file %s", type, file); + if (0 != stat(file, st)) + { + D("No %s file.", file); + perror_msg("Unable to stat %s", file); + ret++; + } + else + { + int status = luaL_loadfile(Rd->L, file), result; + + if (status) + { + bitch(Rd, "No such thing.", "Can't load file."); + E("Couldn't load file: %s", lua_tostring(Rd->L, -1)); + ret++; + } + else + { + result = lua_pcall(Rd->L, 0, LUA_MULTRET, 0); + + if (result) + { + bitch(Rd, "Broken thing.", "Can't run file."); + E("Failed to run script: %s", lua_tostring(Rd->L, -1)); + ret++; + } + else + { + lua_getglobal(Rd->L, var); + lua_pushnil(Rd->L); + + while(lua_next(Rd->L, -2) != 0) + { + char *n = (char *) lua_tostring(Rd->L, -2); + + if (lua_isstring(Rd->L, -1)) + { + tnm->putstr(tnm, n, (char *) lua_tostring(Rd->L, -1)); +d("Lua reading (%s) %s = %s", type, n, getStrH(tnm, n)); + } + else + { + char *v = (char *) lua_tostring(Rd->L, -1); + W("Unknown Lua variable type for %s = %s", n, v); + } + lua_pop(Rd->L, 1); + } + + if (-1 == clock_gettime(CLOCK_REALTIME, now)) + perror_msg("Unable to get the time."); + double n = (now->tv_sec * 1000000000.0) + now->tv_nsec; + double t = (then.tv_sec * 1000000000.0) + then.tv_nsec; + T("Reading %s file took %lf seconds", type, (n - t) / 1000000000.0); + } + } + } + + return ret; +} + + +char *checkLinky(reqData *Rd) +{ +// TODO - should be from Rd.shs->linky-hashish + char *ret = xstrdup(""), *t0 = getStrH(Rd->stuff, "linky-hashish"); + + if ('\0' != t0[0]) + { + char *t1 = qurl_encode(t0, strlen(t0)); + free(ret); + ret = xmprintf("You have an email waiting with a validation link in it, please check your email. " + "It will be from %s@%s, and it might be in your spam folder, coz these sorts of emails sometimes end up there. " + "You should add that email address to your contacts, or otherwise let it through your spam filter. " +// "%s" + "
\n", + "grid_no_reply", Rd->Host, + Rd->Host, Rd->RUri +// ,t1, t0 + ); + free(t1); + } + return ret; +} + + +static void freeSesh(reqData *Rd, boolean linky, boolean wipe) +{ + char *file = NULL; + sesh *shs = &Rd->shs; + +T("free sesh %s %s", linky ? "linky" : "session", wipe ? "wipe" : "delete"); + if (linky) + { + shs = Rd->lnk; + file = xmprintf("%s/sessions/%s.linky", scCache, shs->leaf); + } + else + file = xmprintf("%s/sessions/%s.lua", scCache, shs->leaf); + + if (wipe) + I("Wiping session %s.", file); + else + I("Deleting session %s.", file); + + if ('\0' != shs->leaf[0]) + { + if (unlink(file)) + perror_msg("Unable to delete %s", file); + } + + Rd->body-> remove(Rd->body, "munchie"); + + freeCookie(Rd, "toke_n_munchie"); + freeCookie(Rd, "hashish"); + cookie *ck = setCookie(Rd, "toke_n_munchie", ""); + cookie *ckh = setCookie(Rd, "hashish", ""); + ck->maxAge = -1; // Should expire immediately. + ckh->maxAge = -1; // Should expire immediately. + + qhashtbl_obj_t obj; + + if (wipe) + { + memset((void*)&obj, 0, sizeof(obj)); + Rd->database->lock(Rd->database); + while(Rd->database->getnext(Rd->database, &obj, false) == true) + Rd->database->remove(Rd->database, obj.name); + Rd->database->unlock(Rd->database); + if (NULL != shs->name) free(shs->name); + shs->name = NULL; + if (NULL != shs->UUID) free(shs->UUID); + shs->UUID = NULL; + shs->level = -256; +// TODO - should I wipe the rest of Rd->shs as well? + Rd->stuff->remove(Rd->stuff, "name"); + Rd->stuff->remove(Rd->stuff, "firstName"); + Rd->stuff->remove(Rd->stuff, "lastName"); + Rd->stuff->remove(Rd->stuff, "email"); + Rd->stuff->remove(Rd->stuff, "passwordSalt"); + Rd->stuff->remove(Rd->stuff, "passwordHash"); + Rd->stuff->remove(Rd->stuff, "passHash"); + Rd->stuff->remove(Rd->stuff, "passSalt"); + Rd->stuff->remove(Rd->stuff, "linky-hashish"); + } + + if (shs->isLinky) + { + free(Rd->lnk); + Rd->lnk = NULL; + } + else + { + shs->leaf[0] = '\0'; + } + free(file); +} + +static void setToken_n_munchie(reqData *Rd, boolean linky) +{ + sesh *shs = &Rd->shs; + char *file; + + if (linky) + { + shs = Rd->lnk; + file = xmprintf("%s/sessions/%s.linky", scCache, shs->leaf); + } + else + { + file = xmprintf("%s/sessions/%s.lua", scCache, shs->leaf); + } + + struct stat st; + int s = stat(file, &st); + + if (!linky) + { + setCookie(Rd, "toke_n_munchie", shs->toke_n_munchie); + setCookie(Rd, "hashish", shs->hashish); + } + char *tnm0 = xmprintf( "toke_n_munchie = \n" + "{\n" + " ['IP']='%s',\n" + " ['salt']='%s',\n" + " ['seshID']='%s',\n", + getStrH(Rd->headers, "REMOTE_ADDR"), + shs->salt, + shs->seshID + ); + char *tnm1 = xmprintf(" ['name']='%s',\n ['level']='%d',\n", shs->name, (int) shs->level); + char *tnm2 = xmprintf(" ['UUID']='%s',\n", shs->UUID); + char *tnm3 = xmprintf(" ['passHash']='%s',\n", getStrH(Rd->stuff, "passHash")); + char *tnm4 = xmprintf(" ['passSalt']='%s',\n", getStrH(Rd->stuff, "passSalt")); + char *tnm9 = xmprintf("}\n" + "return toke_n_munchie\n"); + int fd = notstdio(xcreate_stdio(file, O_CREAT | O_WRONLY | O_TRUNC | O_CLOEXEC, S_IRUSR | S_IWUSR)); + size_t l = strlen(tnm0); + + if (s) + I("Creating session %s.", file); + else + C("Updating session %s.", file); // I don't think updates can occur now. +t("Write shs %s", tnm0); + if (l != writeall(fd, tnm0, l)) + { + perror_msg("Writing %s", file); + freeSesh(Rd, linky, TRUE); + } + + if (NULL != shs->name) + { +t("Write shs %s", tnm1); + l = strlen(tnm1); + if (l != writeall(fd, tnm1, l)) + { + perror_msg("Writing %s", file); + freeSesh(Rd, linky, TRUE); + } + } + if (NULL != shs->UUID) + { +t("Write shs %s", tnm2); + l = strlen(tnm2); + if (l != writeall(fd, tnm2, l)) + { + perror_msg("Writing %s", file); + freeSesh(Rd, linky, TRUE); + } + } + + if ('\0' != getStrH(Rd->stuff, "passHash")[0]) + { +t("Write shs %s", tnm3); + l = strlen(tnm3); + if (l != writeall(fd, tnm3, l)) + { + perror_msg("Writing %s", file); + freeSesh(Rd, linky, TRUE); + } + } + + if ('\0' != getStrH(Rd->stuff, "passSalt")[0]) + { +t("Write shs %s", tnm4); + l = strlen(tnm4); + if (l != writeall(fd, tnm4, l)) + { + perror_msg("Writing %s", file); + freeSesh(Rd, linky, TRUE); + } + } + + l = strlen(tnm9); + if (l != writeall(fd, tnm9, l)) + { + perror_msg("Writing %s", file); + freeSesh(Rd, linky, TRUE); + } + // Set the mtime on the file. + futimens(fd, shs->timeStamp); + xclose(fd); + free(tnm9); + free(tnm4); + free(tnm3); + free(tnm2); + free(tnm1); + free(tnm0); + free(file); + + if (linky) + { +// TODO - Later use libcurl. + char *first = getStrH(Rd->stuff, "firstName"), *last = getStrH(Rd->stuff, "lastName"); +// TODO - should be from Rd.shs->linky-hashish + char *t0 = xstrdup(Rd->lnk->hashish), *content, *command; + + if ('\0' != t0[0]) + { + size_t sz = qhex_decode(t0); + char *t1 = qB64_encode(t0, sz); + + content = xmprintf( + "From: grid_no_reply@%s\n" + "Relpy-to: grid_no_reply@%s\n" + "Return-Path: bounce_email@%s\n" + "To: %s\n" + "Subject: Validate your new account on %s\n" + "\n" + "This is an automated validation email sent from %s.\n" + "\n" + "Dear %s %s,\n" + "\n" + "Some one has created the account '%s %s' on \n" + "https://%s%s, and hopefully it was you.\n" + "If it wasn't you, you can ignore this email.\n" + "\n" + "Please go to this web link to validate your new account -\n" + "https://%s%s?hashish=%s\n" + "\n" + "Do not reply to this email.\n" + "\n", + Rd->Host, Rd->Host, Rd->Host, + getStrH(Rd->stuff, "email"), + Rd->Host, Rd->Host, + first, last, + first, last, Rd->Host, Rd->RUri, + Rd->Host, Rd->RUri, t1 + ); + l = strlen(content); + file = xmprintf("%s/sessions/%s.email", scCache, shs->leaf); + fd = notstdio(xcreate_stdio(file, O_CREAT | O_WRONLY | O_TRUNC | O_CLOEXEC, S_IRUSR | S_IWUSR)); + + if (l != writeall(fd, content, l)) + { + perror_msg("Writing %s", file); +// freeSesh(Rd, linky, TRUE); + } + xclose(fd); + I("Sending linky email to %s %s", getStrH(Rd->stuff, "email"), t1); + command = xmprintf("sendmail -oi -t <'%s'", file); + int i = system(command); + if (!WIFEXITED(i)) + E("sendmail command failed!"); + free(command); + free(file); + free(content); + free(t1); + free(t0); + } + } +} + + +static void generateAccountUUID(reqData *Rd) +{ + // Generate a UUID, check it isn't already being used. + char uuid[37], *where; + uuid_t binuuid; + my_ulonglong users = 0; + int c; + + do // UserAccounts.PrincipalID is a unique primary index anyway, but we want the user creation process to be a little on the slow side. + { + struct stat st; + + uuid_generate_random(binuuid); + uuid_unparse_lower(binuuid, uuid); + // Try Lua user file. + where = xmprintf("%s/users/%s.lua", scData, uuid); + c = stat(where, &st); + if (c) + users = 1; + free(where); + // Try database. + where = xmprintf("UserAccounts.PrincipalID = '%s'", uuid); + D("Trying new UUID %s.", where); + users = dbCount("UserAccounts", where); + free(where); + } while (users != 0); + if (NULL != Rd->shs.UUID) free(Rd->shs.UUID); + Rd->shs.UUID = xstrdup(uuid); + Rd->shs.level = -200; + Rd->database->putstr(Rd->database, "UserAccounts.PrincipalID", uuid); + Rd->database->putstr(Rd->database, "UserAccounts.Userlevel", "-200"); +} + +char *getLevel(short level) +{ + char *ret = "", *lvl = xmprintf("%d", level); + ret = accountLevels->getstr(accountLevels, lvl, false); + if (NULL == ret) + { + qlisttbl_obj_t obj; + + memset((void*)&obj, 0, sizeof(obj)); // must be cleared before call + accountLevels->lock(accountLevels); + while(accountLevels->getnext(accountLevels, &obj, NULL, false) == true) + { + if (atoi(obj.name) <= level) + ret = (char *) obj.data; + } + } + free(lvl); + return ret; +} + +typedef struct _systemFolders systemFolders; +struct _systemFolders +{ + char *name; + short type; +}; + +systemFolders sysFolders[] = +{ + {"My Inventory", 8}, + {"Animations", 20}, + {"Body Parts", 13}, + {"Calling Cards", 2}, +// {"Friends", 2}, +// {"All", 2}, + {"Clothing", 5}, + {"Current Outfit", 46}, + {"Favorites", 23}, + {"Gestures", 21}, + {"Landmarks", 3}, + {"Lost And Found", 16}, + {"Mesh", 49}, + {"My Outfits", 48}, + {"My Suitcase", 100}, // All the others are replicated inside. + {"Notecards", 7}, + {"Objects", 6}, + {"Outfit", 47}, + {"Photo Album", 15}, + {"Scripts", 10}, + {"Sounds", 1}, + {"Textures", 0}, + {"Trash", 14}, + {NULL, -1} +}; + +boolean writeLuaDouble(reqData *Rd, int fd, char *file, char *name, double number) +{ + boolean ret = TRUE; +// 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. + char *t = xmprintf(" ['%s'] = '%f',\n", name, number); // NOTE - default precision is 6 decimal places. + size_t l = strlen(t); + + if (l != writeall(fd, t, l)) + { + perror_msg("Writing %s", file); + ret = FALSE; + } + free(t); + return ret; +} + +boolean writeLuaInteger(reqData *Rd, int fd, char *file, char *name, long number) +{ + boolean ret = TRUE; +// 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. + char *t = xmprintf(" ['%s'] = '%ld',\n", name, number); + size_t l = strlen(t); + + if (l != writeall(fd, t, l)) + { + perror_msg("Writing %s", file); + ret = FALSE; + } + free(t); + return ret; +} + +boolean writeLuaString(reqData *Rd, int fd, char *file, char *name, char *string) +{ + boolean ret = TRUE; + + if (NULL == string) + string = getStrH(Rd->stuff, name); + + size_t l = strlen(string); + char *t0 = xmalloc(l * 2 + 1); + int i, j = 0; + +// TODO - maybe escape other non printables as well? + for (i = 0; i < l; i++) + { + // We don't need to escape [] here, coz we are using '' below. Same applies to ", but do it anyway. + switch(string[i]) + { + case '\n': + case '\\': + case '\'': + case '"': + t0[j++] = '\\'; break; + } + if ('\n' == string[i]) + t0[j++] = 'n'; + else if ('\r' == string[i]) + ; + else + t0[j++] = string[i]; + } + t0[j] = '\0'; + + char *t1 = xmprintf(" ['%s'] = '%s',\n", name, t0); + + l = strlen(t1); + if (l != writeall(fd, t1, l)) + { + perror_msg("Writing %s to %s", name, file); + ret = FALSE; + } + free(t1); + free(t0); + return ret; +} + +static void accountWrite(reqData *Rd) +{ + char *uuid = getStrH(Rd->database, "UserAccounts.PrincipalID"); + char *file = xmprintf("%s/users/%s.lua", scData, uuid); + char *level = getStrH(Rd->database, "UserAccounts.UserLevel"); + char *link = (NULL == Rd->lnk) ? "" : Rd->lnk->hashish; + char *tnm = "user = \n{\n"; + + struct stat st; + int s = stat(file, &st); + int fd = notstdio(xcreate_stdio(file, O_CREAT | O_WRONLY | O_TRUNC | O_CLOEXEC, S_IRUSR | S_IWUSR)); + size_t l = strlen(tnm); + uuid_t binuuid; + + uuid_clear(binuuid); + if ((NULL != uuid) && ('\0' != uuid[0])) + uuid_parse(uuid, binuuid); + if ((NULL != uuid) && ('\0' != uuid[0]) && (!uuid_is_null(binuuid))) + { + if (s) + I("Creating user %s.", file); + else + I("Updating user %s.", file); + + if (l != writeall(fd, tnm, l)) + perror_msg("Writing %s", file); + else + { + char *name = Rd->stuff->getstr(Rd->stuff, "name", true); + char *end = "}\nreturn user\n"; + + if (!writeLuaString (Rd, fd, file, "name", name)) goto notWritten; + if (!writeLuaInteger(Rd, fd, file, "created", + (strcmp("", getStrH(Rd->stuff, "created")) != 0) ? atol(getStrH(Rd->stuff, "created")) : (long) Rd->shs.timeStamp[1].tv_sec)) goto notWritten; + if (!writeLuaString (Rd, fd, file, "email", NULL)) goto notWritten; + if (!writeLuaString (Rd, fd, file, "title", getLevel(atoi(level)))) goto notWritten; + if (!writeLuaString (Rd, fd, file, "level", level)) goto notWritten; + if (!writeLuaInteger(Rd, fd, file, "flags", 0)) goto notWritten; + if (!writeLuaInteger(Rd, fd, file, "active", 1)) goto notWritten; + if (!writeLuaString (Rd, fd, file, "passwordHash", NULL)) goto notWritten; + if (!writeLuaString (Rd, fd, file, "passwordSalt", NULL)) goto notWritten; + if (!writeLuaString (Rd, fd, file, "UUID", uuid)) goto notWritten; + if (!writeLuaString (Rd, fd, file, "DoB", NULL)) goto notWritten; + if (!writeLuaString (Rd, fd, file, "agree", NULL)) goto notWritten; + if (!writeLuaString (Rd, fd, file, "adult", NULL)) goto notWritten; + if (!writeLuaString (Rd, fd, file, "aboutMe", NULL)) goto notWritten; + if (!writeLuaString (Rd, fd, file, "vouched", "off")) goto notWritten; + if (!writeLuaString (Rd, fd, file, "voucher", NULL)) goto notWritten; + if (!writeLuaString (Rd, fd, file, "link", link)) goto notWritten; + l = strlen(end); + if (l != writeall(fd, end, l)) + perror_msg("Writing %s", file); + else + { + char *nm = xmprintf("%s/users/%s.lua", scData, qstrreplace("tr", name, " ", "_")); + + free(file); + file = xmprintf("%s.lua", uuid); + I("Symlinking %s to %s", file, nm); + if (0 != symlink(file, nm)) + perror_msg("Symlinking %s to %s", file, nm); + free(nm); + } +notWritten: + free(name); + } + xclose(fd); + + short lvl = atoi(level); + + if (0 <= lvl) // Note that http://opensimulator.org/wiki/Userlevel claims that 1 and above are "GOD_LIKE". + { + if (Rd->fromDb) + { + I("Updating database user %s.", getStrH(Rd->stuff, "name")); + } + else + { + // Setup the database stuff. + static dbRequest *acntsI = NULL; + if (NULL == acntsI) + { + static char *szi[] = + { + "FirstName", + "LastName", + "Email", + "Created", + "PrincipalID", + "ScopeID", + "UserLevel", + "UserFlags", + "UserTitle", +// "ServiceURLs", // No worky "text", filled with crap. + "active", + NULL + }; + static char *szo[] = {NULL}; + acntsI = xzalloc(sizeof(dbRequest)); + acntsI->table = "UserAccounts"; + acntsI->inParams = szi; + acntsI->outParams = szo; + acntsI->where = ""; + acntsI->type = CT_CREATE; + dbRequests->addfirst(dbRequests, &acntsI, sizeof(dbRequest *)); + } + static dbRequest *authI = NULL; + if (NULL == authI) + { + static char *szi[] = {"UUID", "passwordSalt", "passwordHash", "accountType", "webLoginKey", NULL}; + static char *szo[] = {NULL}; + authI = xzalloc(sizeof(dbRequest)); + authI->table = "auth"; + authI->inParams = szi; + authI->outParams = szo; + authI->where = ""; + authI->type = CT_CREATE; + dbRequests->addfirst(dbRequests, &authI, sizeof(dbRequest *)); + } + static dbRequest *invFolderI = NULL; + if (NULL == invFolderI) + { + static char *szi[] = + { + "agentID", + "folderName", + "type", // smallint(6) + "version", // int(11) + "folderID", + "parentFolderID", + NULL + }; + static char *szo[] = {NULL}; + invFolderI = xzalloc(sizeof(dbRequest)); + invFolderI->table = "inventoryfolders"; + invFolderI->inParams = szi; + invFolderI->outParams = szo; + invFolderI->where = ""; + invFolderI->type = CT_CREATE; + dbRequests->addfirst(dbRequests, &invFolderI, sizeof(dbRequest *)); + } + static dbRequest *gUserI = NULL; + if (NULL == gUserI) + { +// static char *szi[] = {"UserID", "HomeRegionID", "HomePosition", "HomeLookAt", "LastRegionID", "LastPosition", "LastLookAt", "Online", "Login", "Logout", NULL}; + static char *szi[] = {"UserID", NULL}; // All the defaults are what we would set anyway. + static char *szo[] = {NULL}; + gUserI = xzalloc(sizeof(dbRequest)); + gUserI->table = "GridUser"; + gUserI->inParams = szi; + gUserI->outParams = szo; + gUserI->where = ""; + gUserI->type = CT_CREATE; + dbRequests->addfirst(dbRequests, &gUserI, sizeof(dbRequest *)); + } + + I("Creating database user %s %s.", uuid, getStrH(Rd->stuff, "name")); + char *first = Rd->stuff->getstr(Rd->stuff, "name", true), *last = strchr(first, ' '); + + // create user record. + *last++ = '\0'; + if (0 != dbDoSomething(acntsI, FALSE, + first, + last, + getStrH(Rd->stuff, "email"), + (strcmp("", getStrH(Rd->stuff, "created")) != 0) ? atoi(getStrH(Rd->stuff, "created")) : (int) Rd->shs.timeStamp[1].tv_sec, + uuid, + "00000000-0000-0000-0000-000000000000", + atoi(level), + 0, + getLevel(atoi(level)), +// "", // Defaults to NULL, empty string seems OK to. Then gets filled in later. + 1 + )) + bitch(Rd, "Internal error.", "Failed to create UserAccounts record."); + else + { + char uuidI[37], uuidR[37], uuidC[37], uuidS[37]; + uuid_t binuuidI; + int r = 0, i; + + // Create inventory records. + strcpy(uuidR, "00000000-0000-0000-0000-000000000000"); + for (i = 0; (NULL != sysFolders[i].name) && (0 == r); i++) + { + uuid_generate_random(binuuidI); + uuid_unparse_lower(binuuidI, uuidI); +// TODO - should check there isn't a folder with this UUID already. + D("Creating %s inventory folder for user %s.", sysFolders[i].name, getStrH(Rd->stuff, "name")); + r += dbDoSomething(invFolderI, FALSE, uuid, sysFolders[i].name, sysFolders[i].type, 1, uuidI, uuidR); // LEAKY + if (0 != r) + bitch(Rd, "Internal error.", "Failed to create invenoryFolder record."); + if (strcmp("My Inventory", sysFolders[i].name) == 0) + strcpy(uuidR, uuidI); + if (strcmp("Calling Cards", sysFolders[i].name) == 0) + strcpy(uuidC, uuidI); + if (strcmp("My Suitcase", sysFolders[i].name) == 0) + strcpy(uuidS, uuidI); + } + + uuid_generate_random(binuuidI); + uuid_unparse_lower(binuuidI, uuidI); +// TODO - should check there isn't a folder with this UUID already. + D("Creating %s inventory folder for user %s.", "Friends", getStrH(Rd->stuff, "name")); + r += dbDoSomething(invFolderI, FALSE, uuid, "Friends", 2, 1, uuidI, uuidC); + if (0 != r) + bitch(Rd, "Internal error.", "Failed to create invenoryFolder record."); + strcpy(uuidC, uuidI); + + uuid_generate_random(binuuidI); + uuid_unparse_lower(binuuidI, uuidI); +// TODO - should check there isn't a folder with this UUID already. + D("Creating %s inventory folder for user %s.", "All", getStrH(Rd->stuff, "name")); + r += dbDoSomething(invFolderI, FALSE, uuid, "All", 2, 1, uuidI, uuidC); + if (0 != r) + bitch(Rd, "Internal error.", "Failed to create invenoryFolder record."); + + for (i = 1; (NULL != sysFolders[i].name) && (0 == r); i++) + { + uuid_generate_random(binuuidI); + uuid_unparse_lower(binuuidI, uuidI); +// TODO - should check there isn't a folder with this UUID already. + D("Creating %s inventory folder for user %s.", sysFolders[i].name, getStrH(Rd->stuff, "name")); + r += dbDoSomething(invFolderI, FALSE, uuid, sysFolders[i].name, sysFolders[i].type, 1, uuidI, uuidS); + if (0 != r) + bitch(Rd, "Internal error.", "Failed to create invenoryFolder record."); + } + + if (0 == r) + { + // Create location record. + D("Creating home and last positions for user %s.", getStrH(Rd->stuff, "name")); + if (0 != dbDoSomething(gUserI, FALSE, uuid)) // LEAKY + bitch(Rd, "Internal error.", "Failed to create GridUser record."); + else + { + // Finally create auth record, so they can log in. + D("Creating auth record for user %s %s.", uuid, getStrH(Rd->stuff, "name")); + if (0 != dbDoSomething(authI, FALSE, uuid, getStrH(Rd->stuff, "passwordSalt"), getStrH(Rd->stuff, "passwordHash"), "UserAccount", "00000000-0000-0000-0000-000000000000")) + bitch(Rd, "Internal error.", "Failed to create auth record."); + } + } + + // load iar -m first last / password /opt/opensim_SC/backups/DefaultMember.IAR + simList *sims = getSims(); + struct sysinfo info; + float la; + + sysinfo(&info); + la = info.loads[0]/65536.0; + + for (i = 0; i < sims->num; i++) + { + char *sim = sims->sims[i], *name = getSimName(sims->sims[i]); + + if (checkSimIsRunning(sim)) + { + I("Loading default member IAR for %s %s in sim %s, this might take a couple of minutes.", first, last, name); + char *c = xmprintf("%s %s/%s send-keys -t '%s:%d' 'load iar -m %s %s / password /opt/opensim_SC/backups/DefaultMember.IAR' Enter", + Tcmd, scRun, Tsocket, Tconsole, i + 1, first, last); +T(c); + int r = system(c); + if (!WIFEXITED(r)) + E("tmux load iar command failed!"); + else + { +// memset(toybuf, 0, sizeof(toybuf)); +// snprintf(toybuf, sizeof(toybuf), "INITIALIZATION COMPLETE FOR %s", name); +// waitTmuxText(name, toybuf); +// I("%s is done starting up.", name); +// la = waitLoadAverage(la, loadAverageInc, simTimeOut); + } + free(c); + free(name); + break; + } + free(name); + } + freeSimList(sims); + } + free(first); + } + } + } + else + W("Not writing NULL UUID user!"); + free(file); +} + +static sesh *newSesh(reqData *Rd, boolean linky) +{ + unsigned char *md5hash = xzalloc(17); + char *toke_n_munchie, *munchie, *hashish, *t0, *t1; + char uuid[37]; + uuid_t binuuid; + sesh *ret = &Rd->shs; + +T("new sesh %s %s %s", linky ? "linky" : "session", ret->UUID, ret->name); + if (linky) + { + Rd->lnk = xzalloc(sizeof(sesh)); + ret = Rd->lnk; + ret->UUID = Rd->shs.UUID; + } + + char buf[128]; // 512 bits. + int numBytes = getrandom((void *)buf, sizeof(buf), GRND_NONBLOCK); + + // NOTE that getrandom() returns random bytes, which may include '\0'. + if (-1 == numBytes) + { + perror_msg("Unable to generate a suitable random number."); + // EAGAIN - not enough entropy, try again. + // EINTR - signal handler interrupted it, try again. + } + else + { + t0 = qhex_encode(buf, sizeof(buf)); + qstrcpy(ret->salt, sizeof(ret->salt), t0); + free(t0); +//d("salt %s", ret->salt); + numBytes = getrandom((void *)buf, sizeof(buf), GRND_NONBLOCK); + if (-1 == numBytes) + perror_msg("Unable to generate a suitable random number."); + else + { + t0 = qhex_encode(buf, sizeof(buf)); + qstrcpy(ret->seshID, sizeof(ret->seshID), t0); + free(t0); +//d("seshID %s", ret->seshID); + + ret->timeStamp[0].tv_nsec = UTIME_OMIT; + ret->timeStamp[0].tv_sec = UTIME_OMIT; + if (-1 == clock_gettime(CLOCK_REALTIME, &ret->timeStamp[1])) + perror_msg("Unable to get the time."); + else + { + // tv_sec is a time_t, tv_nsec is a long, but the actual type of time_t isn't well defined, it's some sort of integer. + t0 = xmprintf("%s%ld.%ld", ret->seshID, (long) ret->timeStamp[1].tv_sec, ret->timeStamp[1].tv_nsec); + qstrcpy(ret->sesh, sizeof(ret->sesh), t0); +//d("sesh %s", ret->sesh); + t1 = myHMAC(t0, FALSE); + free(t0); + munchie = xmprintf("%s%ld.%ld", t1, (long) ret->timeStamp[1].tv_sec, ret->timeStamp[1].tv_nsec); + free(t1); + qstrcpy(ret->munchie, sizeof(ret->munchie), munchie); +//d("munchie %s", ret->munchie); +// TODO - chicken and egg? Used to be from stuff->UUID. + t1 = ret->UUID; + if (NULL == t1) + { + uuid_clear(binuuid); + uuid_unparse_lower(binuuid, uuid); + ret->UUID = xstrdup(uuid); + } + t0 = xmprintf("%s%s", ret->UUID, munchie); + free(munchie); + toke_n_munchie = myHMAC(t0, FALSE); + free(t0); + qstrcpy(ret->toke_n_munchie, sizeof(ret->toke_n_munchie), toke_n_munchie); +//d("toke_n_munchie %s", ret->toke_n_munchie); + hashish = myHMACkey(ret->salt, toke_n_munchie, FALSE); + free(toke_n_munchie); + qstrcpy(ret->hashish, sizeof(ret->hashish), hashish); +//d("hashish %s", ret->hashish); + t0 = myHMACkey(getStrH(Rd->configs, "pepper"), hashish, TRUE); + free(hashish); + qstrcpy(ret->leaf, sizeof(ret->leaf), t0); +//d("leaf %s", ret->leaf); + free(t0); + ret->isLinky = linky; + setToken_n_munchie(Rd, linky); + } + } + } + + free(md5hash); + return ret; +} + + + + +/* CRUD (Create, Read, Update, Delete) +CRAP (Create, Replicate, Append, Process) +Though I prefer - +DAVE (Delete, Add, View, Edit), coz the names are shorter. B-) +On the other hand, list or browse needs to be added, which is why they have +BREAD (Browse, Read, Edit, Add, Delete) +CRUDL (Create, Read, Update, Delete, List) +CRUDE (Create, Read, Update, Delete, Experience) +Maybe - +DAVEE (Delete, Add, View, Edit, Explore) +*/ + +// lua.h has LUA_T* NONE, NIL, BOOLEAN, LIGHTUSERDATA, NUMBER, STRING, TABLE, FUNCTION, USERDATA, THREAD as defines, -1 - 8. +// These are the missing ones. Then later we will have prim, mesh, script, sound, terrain, ... +#define LUA_TGROUP 42 +#define LUA_TINTEGER 43 +#define LUA_TEMAIL 44 +#define LUA_TPASSWORD 45 +#define LUA_TFILE 46 +#define LUA_TIMAGE 47 + +#define FLD_NONE 0 +#define FLD_EDITABLE 1 +#define FLD_HIDDEN 2 +#define FLD_REQUIRED 4 + +typedef struct _inputField inputField; +typedef struct _inputSub inputSub; +typedef struct _inputForm inputForm; +typedef struct _inputValue inputValue; + +typedef int (*inputFieldValidFunc) (reqData *Rd, inputForm *iF, inputValue *iV); +typedef void (*inputFieldShowFunc) (reqData *Rd, inputForm *iF, inputValue *iV); +typedef int (*inputSubmitFunc) (reqData *Rd, inputForm *iF, inputValue *iV); +typedef void (*inputFormShowFunc) (reqData *Rd, inputForm *iF, inputValue *iV); + +struct _inputField +{ + char *name, *title, *help; + inputFieldValidFunc validate; // Alas C doesn't have any anonymous function standard. + inputFieldShowFunc web, console, gui; + inputField **group; // If this is a LUA_TGROUP, then this will be a null terminated array of the fields in the group. +// database details +// lua file details + signed char type, flags; + short editLevel, viewLevel, viewLength, maxLength; +}; +struct _inputSub +{ + char *name, *title, *help, *outputForm; + inputSubmitFunc submit; +}; +struct _inputForm +{ + char *name, *title, *help; + qlisttbl_t *fields; // qlisttbl coz iteration in order and lookup are important. + qhashtbl_t *subs; + inputFormShowFunc web, eWeb; // display web, console, gui; +// read function +// write function +}; +struct _inputValue +{ + inputField *field; + void *value; // If this is a LUA_TGROUP, then this will be a null. + short valid; // 0 for not yet validated, negative for invalid, positive for valid. + short source, index; +}; + + +static int sessionValidate(reqData *Rd, inputForm *iF, inputValue *iV) +{ + int ret = 0; + boolean linky = FALSE; + char *toke_n_munchie = "", *munchie = "", *hashish = "", *leaf = "", *timeStamp = "", *seshion = "", *seshID = "", *t0, *t1; + + // In this case the session stuff has to come from specific places. + hashish = Rd->queries->getstr(Rd->queries, "hashish", true); +//d("O hashish %s", hashish); + if (NULL != hashish) + { + char *t = xstrdup(hashish); + size_t sz = qB64_decode(t); + +// TODO - should validate the cookie version as well, if it was sent. +// 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. +// Though only if there's a munchie in the body? + I("Validating LINKY hashish %s", hashish); + free(hashish); + hashish = qhex_encode(t, sz); + free(t); + linky = TRUE; + } + else + { + toke_n_munchie = getStrH(Rd->cookies, "toke_n_munchie"); +// munchie = getStrH(Rd->body, "munchie"); + hashish = Rd->cookies->getstr(Rd->cookies, "hashish", true); + if (('\0' == toke_n_munchie[0]) || ((NULL == hashish))) + { + if (strcmp("logout", Rd->doit) == 0) + { + I("Not checking session, coz we are logging out."); + Rd->shs.status = SHS_NUKE; + return ret; + } + bitchSession(Rd, "Invalid session.", "No or blank hashish or toke_n_munchie."); + Rd->shs.status = SHS_NONE; + ret++; + } + else + I("Validating SESSION hashish %s", hashish); + } + +//d("O toke_n_munchie %s", toke_n_munchie); +//d("O munchie %s", munchie); + if (0 == ret) + { + struct stat st; + struct timespec now; + + leaf = myHMACkey(getStrH(Rd->configs, "pepper"), hashish, TRUE); +//d("leaf %s", leaf); + if (linky) + t0 = xmprintf("%s/sessions/%s.linky", scCache, leaf); + else + t0 = xmprintf("%s/sessions/%s.lua", scCache, leaf); + + qhashtbl_t *tnm = qhashtbl(0, 0); + ret = LuaToHash(Rd, t0, "toke_n_munchie", tnm, ret, &st, &now, "session"); + free(t0); + + if (0 != ret) + { + // This 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. + // They might also have a stale doit and form cookie. +// bitchSession(Rd, "Invalid session.", "No session file."); + bitchSession(Rd, "", "No session file."); + ret++; + } + else + { + // This is apparently controversial, I added it coz some of the various security docs suggested it's a good idea. + // https://security.stackexchange.com/questions/139952/why-arent-sessions-exclusive-to-an-ip-address?rq=1 + // Includes various reasons why it's bad. + // Another good reason why it is bad, TOR. + // So should make this a user option, like Mantis does. + if (strcmp(getStrH(Rd->headers, "REMOTE_ADDR"), getStrH(tnm, "IP")) != 0) + { + bitchSession(Rd, "Wrong IP for session.", "Session IP doesn't match."); + ret++; + } + else + { + timeStamp = xmprintf("%ld.%ld", (long) st.st_mtim.tv_sec, st.st_mtim.tv_nsec); +//d("timeStamp %s", timeStamp); + seshion = xmprintf("%s%s", tnm->getstr(tnm, "seshID", false), timeStamp); +//d("sesh %s", seshion); + t0 = myHMAC(seshion, FALSE); + munchie = xmprintf("%s%s", t0, timeStamp); +//d("munchie %s", munchie); + free(t0); + free(timeStamp); + t1 = getStrH(Rd->body, "munchie"); + if ('\0' != t1[0]) + { + if (strcmp(t1, munchie) != 0) + { +// 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. + bitchSession(Rd, "Wrong munchie for session, may have been eaten, please try again.", "HMAC(seshID + timeStamp) != munchie"); + ret++; + } + else + { + t0 = xmprintf("%s%s", getStrH(tnm, "UUID"), munchie); + t1 = myHMAC(t0, FALSE); + free(t0); + +//d("toke_n_munchie %s", t1); + if (strcmp(t1, toke_n_munchie) != 0) + { + bitchSession(Rd, "Wrong toke_n_munchie for session.", "HMAC(UUID + munchie) != toke_n_munchie"); + ret++; + } + free(t1); + } + + if (linky) + { + t0 = xmprintf("%s%s", getStrH(tnm, "UUID"), munchie); + t1 = myHMAC(t0, FALSE); + free(t0); + toke_n_munchie = t1; +//d("toke_n_munchie %s", t1); + } + t1 = myHMACkey(getStrH(tnm, "salt"), toke_n_munchie, FALSE); +//d("hashish %s", t1); + if (strcmp(t1, hashish) != 0) + { + bitchSession(Rd, "Wrong hashish for session.", "HMAC(toke_n_munchie + salt) != hashish"); + ret++; + } + free(t1); + } + +// TODO - should carefully review all of this, especially the moving of session data to and fro. + if (0 == ret) + { +W("Validated session."); + sesh *shs = &Rd->shs; + + qstrcpy(shs->leaf, sizeof(shs->leaf), leaf); + if (NULL != shs->name) free(shs->name); + shs->name = tnm->getstr(tnm, "name", true); + if (NULL != shs->UUID) free(shs->UUID); + shs->UUID = tnm->getstr(tnm, "UUID", true); + if (linky) + { +W("Validated session linky."); + addStrL(Rd->messages, "Congratulations, you have validated your new account. Now you can log onto the web site."); + addStrL(Rd->messages, "NOTE - you wont be able to log onto the grid until your new account has been approved."); + Rd->lnk = xzalloc(sizeof(sesh)); + Rd->lnk->status = SHS_NUKE; + qstrcpy(Rd->lnk->leaf, sizeof(Rd->lnk->leaf), leaf); + freeSesh(Rd, linky, FALSE); + qstrcpy(Rd->lnk->leaf, sizeof(Rd->lnk->leaf), ""); + Rd->doit = "validate"; + Rd->output = "accountLogin"; + Rd->form = "accountLogin"; +// TODO - we might want to delete their old .lua session as well. Maybe? Don't think we have any suitable codes to find it. + } + else + { + char *level = tnm->getstr(tnm, "level", false); + + // Check for session timeouts etc. + if (now.tv_sec > st.st_mtim.tv_sec + seshTimeOut) + { + bitch(Rd, "Session timed out.", "No activity for longer than seshTimeOut, session is ancient."); + ret++; + Rd->shs.status = SHS_ANCIENT; + } + else if (now.tv_sec > st.st_mtim.tv_sec + idleTimeOut) + { + bitch(Rd, "Session idled out.", "No activity for longer than idleTimeOut, session is idle."); + ret++; + Rd->shs.status = SHS_IDLE; + } + else if (now.tv_sec > st.st_mtim.tv_sec + seshRenew) + { + D("Session needs renewing."); + Rd->shs.status = SHS_RENEW; + } + else + Rd->shs.status = SHS_VALID; + + if (NULL == level) + level = "-256"; + qstrcpy(shs->sesh, sizeof(shs->sesh), seshion); + qstrcpy(shs->toke_n_munchie, sizeof(shs->toke_n_munchie), toke_n_munchie); + qstrcpy(shs->hashish, sizeof(shs->hashish), hashish); + qstrcpy(shs->munchie, sizeof(shs->munchie), munchie); + qstrcpy(shs->salt, sizeof(shs->salt), tnm->getstr(tnm, "salt", false)); + qstrcpy(shs->seshID, sizeof(shs->seshID), tnm->getstr(tnm, "seshID", false)); + shs->level = atoi(level); +// TODO - get level from somewhere and stuff it in shs. + shs->timeStamp[0].tv_nsec = UTIME_OMIT; + shs->timeStamp[0].tv_sec = UTIME_OMIT; + memcpy(&shs->timeStamp[1], &st.st_mtim, sizeof(struct timespec)); + } + } + + qhashtbl_obj_t obj; + + memset((void*)&obj, 0, sizeof(obj)); + tnm->lock(tnm); + while(tnm->getnext(tnm, &obj, false) == true) + { + char *n = obj.name; + + if ((strcmp("salt", n) != 0) && (strcmp("seshID", n) != 0) && (strcmp("UUID", n) != 0)) + { +t("SessionValidate() Lua read %s = %s", n, (char *) obj.data); + Rd->stuff->putstr(Rd->stuff, obj.name, (char *) obj.data); + } + } + tnm->unlock(tnm); + +// TODO - check this. +// Rd->database->putstr(Rd->database, "UserAccounts.PrincipalID", tnm->getstr(tnm, "UUID", false)); + } + free(munchie); + free(seshion); + } + free(leaf); + tnm->free(tnm); + free(hashish); + } + + return ret; +} + +static void sessionWeb(reqData *Rd, inputForm *iF, inputValue *iV) +{ + HTMLhidden(Rd->reply, iV->field->name, iV->value); +} + +/* +static int UUIDValidate(reqData *Rd, inputForm *iF, inputValue *iV) +{ + int ret = 0; + char *UUID = (char *) iV->value; + + if (36 != strlen(UUID)) + { + bitch(Rd, "Internal error.", "UUID isn't long enough."); + ret++; + } +// TODO - check the characters and dashes as well. + + if (0 == ret) + Rd->stuff->putstr(Rd->stuff, "UUID", UUID); + return ret; +} + +static void UUIDWeb(reqData *Rd, inputForm *iF, inputValue *iV) +{ + HTMLhidden(Rd->reply, iV->field->name, iV->value); +} +*/ + +static int nameValidate(reqData *Rd, inputForm *iF, inputValue *iV) +{ + int ret = 0; + unsigned char *name; // We have to be unsigned coz of isalnum(). + char *where = NULL; + + name = xstrdup(iV->value); + + if ((NULL == name) || ('\0' == name[0])) + { + bitch(Rd, "Please supply an account name.", "None supplied."); + ret++; + } + else + { + int l0 = strlen(name), l1 = 0, l2 = 0; + + if (0 == l0) + { + bitch(Rd, "Please supply an account name.", "Name is empty."); + ret++; + } + else + { + int i; + unsigned char *s = NULL; + + for (i = 0; i < l0; i++) + { + if (isalnum(name[i]) == 0) + { + + if ((' ' == name[i] /*&& (NULL == s)*/)) + { + s = &name[i]; + *s++ = '\0'; + while(' ' == *s) + { + i++; + s++; + } + l1 = strlen(name); + l2 = strlen(s); + + // Apparently names are not case sensitive on login, but stored with case in the database. + // I confirmed that, can log in no matter what case you use. + // Seems to be good security for names to be case insensitive. + // UserAccounts FirstName and LastName fields are both varchar(64) utf8_general_ci. + // The MySQL docs say that the "_ci" bit means comparisons will be case insensitive. So that should work fine. + + // SL docs say 31 characters each for first and last name. UserAccounts table is varchar(64) each. userinfo has varchar(50) for the combined name. + // The userinfo table seems to be obsolete. + // Singularity at least limits the total name to 64. + // I can't find any limitations on characters allowed, but I only ever see letters and digits used. Case is stored, but not significant. + // OpenSims "create user" console command doesn't sanitize it at all, even crashing on some names. + } + else + { + bitch(Rd, "First and last names are limited to ordinary letters and digits, no special characters or fonts.", ""); + ret++; + break; + } +// TODO - compare first, last, and fullname against god names, complain and fail if there's a match. + } + } + + if (NULL == s) + { + bitch(Rd, "Account names have to be two words.", ""); + ret++; + } + if ((31 < l1) || (31 < l2)) + { + bitch(Rd, "First and last names are limited to 31 letters each.", ""); + ret++; + } + if ((0 == l1) || (0 == l2)) + { + bitch(Rd, "First and last names have to be one or more ordinary letters or digits each.", ""); + ret++; + } + + if (0 == ret) + { + Rd->stuff->putstr(Rd->stuff, "firstName", name); + Rd->stuff->putstr(Rd->stuff, "lastName", s); + Rd->stuff->putstrf(Rd->stuff, "name", "%s %s", name, s); + } + } + } + free(name); + + return ret; +} + +static void nameWeb(reqData *Rd, inputForm *oF, inputValue *oV) +{ + if (oV->field->flags & FLD_HIDDEN) + HTMLhidden(Rd->reply, oV->field->name, oV->value); + else + HTMLtext(Rd->reply, "text", oV->field->title, oV->field->name, oV->value, oV->field->viewLength, oV->field->maxLength, oV->field->flags & FLD_REQUIRED); +} + + +static int passwordValidate(reqData *Rd, inputForm *iF, inputValue *iV) +{ + int ret = 0; + char *password = (char *) iV->value, *salt = getStrH(Rd->stuff, "passSalt"), *hash = getStrH(Rd->stuff, "passHash"); + + if ((NULL == password) || ('\0' == password[0])) + { + bitch(Rd, "Please supply a password.", "Password empty or missing."); + ret++; + } + else if (('\0' != salt[0]) && ('\0' != hash[0]) && (strcmp("psswrd", iV->field->name) == 0)) + { + D("Comparing passwords. %s %s %s", password, salt, hash); + char *h = checkSLOSpassword(Rd, salt, password, hash, "Passwords are not the same."); + + if (NULL == h) + ret++; + else + free(h); + } + +// TODO - once the password is validated, store it as the salt and hash. +// If it's an existing account, compare it? Or do that later? + if (0 == ret) + Rd->stuff->putstr(Rd->stuff, "password", password); + + return ret; +} + +static void passwordWeb(reqData *Rd, inputForm *oF, inputValue *oV) +{ + HTMLtext(Rd->reply, "password", oV->field->title, oV->field->name, "", oV->field->viewLength, oV->field->maxLength, oV->field->flags & FLD_REQUIRED); + Rd->reply->addstr(Rd->reply, "While viewers will usually remember your name and password for you, you'll need to remember it for this web site to. " + "I highly recommend using a password manager. KeePass and it's variations is a great password manager.
\n"); +} + +static int emailValidate(reqData *Rd, inputForm *iF, inputValue *iV) +{ +// inputField **group = iV->field->group; + int ret = 0, i; + boolean notSame = FALSE; + + i = iV->index; + if (2 == i) + { + char *email = (char *) iV->value; + char *emayl = (char *) (iV + 1)->value; + + if ((NULL == email) || (NULL == emayl) || ('\0' == email[0]) || ('\0' == emayl[0])) + { + bitch(Rd, "Please supply an email address.", "None supplied."); + ret++; + } + else if (strcmp(email, emayl) != 0) + { + bitch(Rd, "Email addresses are not the same.", ""); + ret++; + notSame = TRUE; + } + else if (!qstr_is_email(email)) + { + bitch(Rd, "Please supply a proper email address.", "Failed qstr_is_email()"); + ret++; + } + else + { +// TODO - do other email checks - does the domain exist, .. + } + + if ((NULL != email) && (NULL != emayl)) + { + char *t0 = qurl_encode(email, strlen(email)); + + // In theory it's the correct thing to do to NOT load email into stuff on failure, + // In practice, that means it wont show the old email and emayl in the create page when they don't match. + if ((0 == ret) || notSame) + Rd->stuff->putstrf(Rd->stuff, "email", "%s", t0); + free(t0); + } + if ((NULL != email) && (NULL != emayl)) + { + char *t1 = qurl_encode(emayl, strlen(emayl)); + + Rd->stuff->putstrf(Rd->stuff, "emayl", "%s", t1); + free(t1); + } + } + + return ret; +} +static void emailWeb(reqData *Rd, inputForm *oF, inputValue *oV) +{ + 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); + Rd->reply->addstrf(Rd->reply, "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. " + "You should add that email address to your contacts, or otherwise let it through your spam filter.
", + "grid_no_reply", Rd->Host); +} + + +char *months[] = +{ + "january", + "february", + "march", + "april", + "may", + "june", + "july", + "august", + "september", + "october", + "november", + "december" +}; +static int DoBValidate(reqData *Rd, inputForm *iF, inputValue *iV) +{ + int ret = 0, i; + char *t0, *t1; +// inputField **group = iV->field->group; + + i = iV->index; + if (2 == i) + { + t0 = (char *) iV->value; + if ((NULL == t0) || ('\0' == t0[0])) + { + bitch(Rd, "Please supply a year of birth.", "None supplied."); + ret++; + } + else + { + i = atoi(t0); +// TODO - get this to use current year instead of 2020. + if ((1900 > i) || (i > 2020)) + { + bitch(Rd, "Please supply a year of birth.", "Out of range."); + ret++; + } + else if (i < 1901) + { + bitch(Rd, "Please supply a proper year of birth.", "Out of range, too old."); + ret++; + } + else if (i >2004) + { + bitch(Rd, "This grid is Adult rated, you are too young.", "Out of range, too young."); + ret++; + } + } + t1 = (char *) (iV + 1)->value; + if ((NULL == t1) || ('\0' == t1[0])) + { + bitch(Rd, "Please supply a month of birth.", "None supplied."); + ret++; + } + else + { + for (i = 0; i < 12; i++) + { + if (strcmp(months[i], t1) == 0) + break; + } + if (12 == i) + { + bitch(Rd, "Please supply a month of birth.", "Out of range"); + ret++; + } + } + + if (0 == ret) + { + Rd->stuff->putstr(Rd->stuff, "year", t0); + Rd->stuff->putstr(Rd->stuff, "month", t1); + Rd->stuff->putstrf(Rd->stuff, "DoB", "%s %s", t0, t1); + } + } + + return ret; +} +static void DoByWeb(reqData *Rd, inputForm *oF, inputValue *oV) +{ + char *tmp = xmalloc(16), *t; + int i, d; + + Rd->reply->addstr(Rd->reply, "\n"); +} +static void DoBWeb(reqData *Rd, inputForm *oF, inputValue *oV) +{ +} + +static int legalValidate(reqData *Rd, inputForm *iF, inputValue *iV) +{ + int ret = 0, i; + char *t; + inputField **group = iV->field->group; + + i = iV->index; + if (2 == i) + { + t = (char *) iV->value; + if ((NULL == t) || (strcmp("on", t) != 0)) + { + bitch(Rd, "You must be an adult to enter this site.", ""); + ret++; + } + else + Rd->stuff->putstr(Rd->stuff, "adult", t); + t = (char *) (iV + 1)->value; + if ((NULL == t) || (strcmp("on", t) != 0)) + { + bitch(Rd, "You must agree to the Terms & Conditions of Use.", ""); + ret++; + } + else + Rd->stuff->putstr(Rd->stuff, "agree", t); + } + + return ret; +} +static void adultWeb(reqData *Rd, inputForm *oF, inputValue *oV) +{ + HTMLcheckBox(Rd->reply, oV->field->name, oV->field->title, !strcmp("on", getStrH(Rd->body, "adult")), oV->field->flags & FLD_REQUIRED); +} +static void agreeWeb(reqData *Rd, inputForm *oF, inputValue *oV) +{ + HTMLcheckBox(Rd->reply, oV->field->name, oV->field->title, !strcmp("on", getStrH(Rd->body, "agree")), oV->field->flags & FLD_REQUIRED); +} +static void legalWeb(reqData *Rd, inputForm *oF, inputValue *oV) +{ +} +static void ToSWeb(reqData *Rd, inputForm *oF, inputValue *oV) +{ + Rd->reply->addstrf(Rd->reply, "Terms of Service
%s\n", getStrH(Rd->configs, "ToS")); +} + +static int voucherValidate(reqData *Rd, inputForm *oF, inputValue *oV) +{ + int ret = 0; + char *voucher = (char *) oV->value; + + if ((NULL == voucher) || ('\0' == voucher[0])) + { + bitch(Rd, "Please fill in the 'Voucher' section.", "None supplied."); + ret++; + } + + if ((0 == ret) && (NULL != voucher)) + Rd->stuff->putstr(Rd->stuff, "voucher", voucher); + + return ret; +} +static void voucherWeb(reqData *Rd, inputForm *oF, inputValue *oV) +{ + HTMLtext(Rd->reply, "text", oV->field->title, oV->field->name, oV->value, oV->field->viewLength, oV->field->maxLength, oV->field->flags & FLD_REQUIRED); +} + +static int aboutMeValidate(reqData *Rd, inputForm *oF, inputValue *oV) +{ + int ret = 0; + char *about = (char *) oV->value; + + if ((NULL == about) || ('\0' == about[0])) + { + bitch(Rd, "Please fill in the 'About me' section.", "None supplied."); + ret++; + } + + if ((0 == ret) && (NULL != about)) + Rd->stuff->putstr(Rd->stuff, "aboutMe", about); + + return ret; +} + +static void aboutMeWeb(reqData *Rd, inputForm *oF, inputValue *oV) +{ + // For maxlength - the MySQL database field is type text, which has a max length of 64 Kilobytes byets, but characters might take up 1 - 4 bytes, and maxlength is in characters. + // For rows and cols, seems a bit broken, I ask for 5/42, I get 6,36. In world it seems to be 7,46 +// TODO - check against the limit for in world profiles, coz this will become that. +// TODO - validate aboutMe, it should not be empty, and should not be longer than 64 kilobytes. + HTMLtextArea(Rd->reply, oV->field->name, oV->field->title, 7, oV->field->viewLength, 4, oV->field->maxLength, "Describe yourself here.", "off", "true", "soft", oV->value, FALSE, FALSE); +} + +static void accountWebHeaders(reqData *Rd, inputForm *oF) //, char *name) +{ + char *linky = checkLinky(Rd); + + HTMLheader(Rd->reply, " account manager"); + Rd->reply->addstrf(Rd->reply, "account manager
\n"); + if (NULL != Rd->shs.name) + { + char *nm = qstrreplace("tr", xstrdup(Rd->shs.name), " ", "+"); + + Rd->reply->addstrf(Rd->reply, "You are %s
\n", Rd->Host, Rd->Script, Rd->Path, nm, Rd->shs.name); + Rd->reply->addstr(Rd->reply, linky); + free(nm); + } + free(linky); + if (0 != Rd->errors->size(Rd->messages)) + HTMLlist(Rd->reply, "messages -", Rd->messages); + if (NULL != oF->help) + Rd->reply->addstrf(Rd->reply, "%s
\n", oF->help); + HTMLform(Rd->reply, "", Rd->shs.munchie); + HTMLhidden(Rd->reply, "form", oF->name); +} + +static void accountWebFields(reqData *Rd, inputForm *oF, inputValue *oV) +{ + int count = oF->fields->size(oF->fields), i; + + for (i = 0; i < count; i++) + { + if (NULL != oV[i].field->web) + oV[i].field->web(Rd, oF, &oV[i]); + if ((NULL != oV[i].field->help) && ('\0' != oV[i].field->help[0])) + Rd->reply->addstrf(Rd->reply, "%s
\n", oV[i].field->help); +//d("accountWebFeilds(%s, %s)", oF->name, oV[i].field->name); + } +} + +static void accountWebSubs(reqData *Rd, inputForm *oF) +{ + qhashtbl_obj_t obj; + + Rd->reply->addstrf(Rd->reply, "\n"); // Stop Enter key on text fields triggering the first submit button. + memset((void*)&obj, 0, sizeof(obj)); // must be cleared before call + oF->subs->lock(oF->subs); + while(oF->subs->getnext(oF->subs, &obj, false) == true) + { + inputSub *sub = (inputSub *) obj.data; + if ('\0' != sub->title[0]) + HTMLbutton(Rd->reply, sub->name, sub->title); +//d("accountWebSubs(%s, %s '%s')", oF->name, sub->name, sub->title); + } + oF->subs->unlock(oF->subs); +} + +static void accountWebFooter(reqData *Rd, inputForm *oF) +{ + if (0 != Rd->errors->size(Rd->errors)) + HTMLlist(Rd->reply, "errors -", Rd->errors); + HTMLformEnd(Rd->reply); + HTMLfooter(Rd->reply); +} + +static void accountAddWeb(reqData *Rd, inputForm *oF, inputValue *oV) +{ + accountWebHeaders(Rd, oF); + accountWebFields(Rd, oF, oV); + accountWebSubs(Rd, oF); + accountWebFooter(Rd, oF); +} + +static void accountLoginWeb(reqData *Rd, inputForm *oF, inputValue *oV) +{ + if (NULL != Rd->shs.name) free(Rd->shs.name); + Rd->shs.name = NULL; + if (NULL != Rd->shs.UUID) free(Rd->shs.UUID); + Rd->shs.UUID = NULL; + accountWebHeaders(Rd, oF); + accountWebFields(Rd, oF, oV); + accountWebSubs(Rd, oF); + accountWebFooter(Rd, oF); +} + +// TODO - accountViewWeb() and accountViewWeb() should view and edit arbitrary accounts the user is not logged in as, +// but limit things based on being that viewed / edited account, and the users level. +static void accountViewWeb(reqData *Rd, inputForm *oF, inputValue *oV) +{ + char *name = getStrH(Rd->database, "Lua.name"), + *level = getStrH(Rd->database, "UserAccounts.UserLevel"), + *email = getStrH(Rd->database, "UserAccounts.Email"), + *voucher = getStrH(Rd->database, "Lua.voucher"), + *about = getStrH(Rd->database, "Lua.aboutMe"); + time_t crtd = atol(getStrH(Rd->database, "UserAccounts.Created")); + + accountWebHeaders(Rd, oF); + accountWebFields(Rd, oF, oV); + Rd->reply->addstrf(Rd->reply, "Name : %s
", name); + Rd->reply->addstrf(Rd->reply, "Title / level : %s / %s
", getLevel(atoi(level)), level); + Rd->reply->addstrf(Rd->reply, "Date of birth : %s
", getStrH(Rd->database, "Lua.DoB")); + Rd->reply->addstrf(Rd->reply, "Created : %s
", ctime(&crtd)); + Rd->reply->addstrf(Rd->reply, "Email : %s
", email); + Rd->reply->addstrf(Rd->reply, "UUID : %s
", getStrH(Rd->database, "UserAccounts.PrincipalID")); + Rd->reply->addstrf(Rd->reply, "Voucher : %s
", voucher); + HTMLtextArea(Rd->reply, "aboutMe", "About", 7, 50, 4, 16384, "", "off", "true", "soft", about, FALSE, TRUE); + accountWebSubs(Rd, oF); + accountWebFooter(Rd, oF); +} + +static void accountEditWeb(reqData *Rd, inputForm *oF, inputValue *oV) +{ + char *name = getStrH(Rd->database, "Lua.name"), + *level = getStrH(Rd->database, "UserAccounts.UserLevel"), + *email = getStrH(Rd->database, "UserAccounts.Email"), + *voucher = getStrH(Rd->database, "Lua.voucher"), + *about = getStrH(Rd->database, "Lua.aboutMe"), + *lvl = getLevel(atoi(level)); + short lv = atoi(level); + + accountWebHeaders(Rd, oF); + accountWebFields(Rd, oF, oV); +// HTMLtext(Rd->reply, "password", "Old password", "password", "", 16, 0, FALSE); +// Rd->reply->addstr(Rd->reply, "Warning, the limit on password length is set by your viewer, some can't handle longer than 16 characters.
\n"); +//// HTMLtext(Rd->reply, "title", "text", "title", getStrH(Rh->stuff, "title"), 16, 64, TRUE); + + HTMLhidden(Rd->reply, "user", name); + Rd->reply->addstrf(Rd->reply, "Name : %s
", name); +// Rd->reply->addstrf(Rd->reply, "Email : %s
", email); + HTMLtextArea(Rd->reply, "aboutMe", "About", 7, 50, 4, 16384, "", "off", "true", "soft", about, FALSE, TRUE); + Rd->reply->addstrf(Rd->reply, "Voucher : %s
", voucher); + + if (200 <= Rd->shs.level) + { + qlisttbl_obj_t obj; + + HTMLselect(Rd->reply, "level", "level"); + memset((void*)&obj, 0, sizeof(obj)); // must be cleared before call + accountLevels->lock(accountLevels); + while(accountLevels->getnext(accountLevels, &obj, NULL, false) == true) + { + boolean is = false; + short l = atoi((char *) obj.name); + + if (strcmp(lvl, (char *) obj.data) == 0) + is = true; + +// if ((is) || ((l <= Rd->shs.level) && (l != -200) && (l != -100) && (l != -50))) // Not above our pay grade, not newbie, validated, nor vouched for. + if ((is) || ((l <= Rd->shs.level) && (lv <= l))) // As per discussions, can't lower level. Do that in the console. + HTMLoption(Rd->reply, (char *) obj.data, is); + } + accountLevels->unlock(accountLevels); + HTMLselectEnd(Rd->reply); + + Rd->reply->addstrf(Rd->reply, ""); + Rd->reply->addstrf(Rd->reply, "
"); + } + else + Rd->reply->addstrf(Rd->reply, "- disabled
- Account cannot log in anywhere.
"); + Rd->reply->addstrf(Rd->reply, "- newbie
- Newly created account, not yet validated.
"); + Rd->reply->addstrf(Rd->reply, "- validated
- Newly created account, they have clicked on the validation link in their validation email.
"); + Rd->reply->addstrf(Rd->reply, "- vouched for
- Someone has vouched for this person.
"); + Rd->reply->addstrf(Rd->reply, "- approved
- This person is approved, and can log into the world.
"); + Rd->reply->addstrf(Rd->reply, "- god
- This is a god admin person.
"); + Rd->reply->addstrf(Rd->reply, "Title / level : %s / %s
", lvl, level); + + accountWebSubs(Rd, oF); + accountWebFooter(Rd, oF); +} + + +static int accountRead(reqData *Rd, char *uuid, char *firstName, char *lastName) +{ + int ret = 0, rt = -1; + struct stat st; + struct timespec now; + qhashtbl_t *tnm = qhashtbl(0, 0); + uuid_t binuuid; + rowData *rows = NULL; + + // Setup the database stuff. + static dbRequest *uuids = NULL; + if (NULL == uuids) + { + static char *szi[] = {"PrincipalID", NULL}; + static char *szo[] = {NULL}; + uuids = xzalloc(sizeof(dbRequest)); + uuids->table = "UserAccounts"; + uuids->inParams = szi; + uuids->outParams = szo; + uuids->where = "PrincipalID=?"; + dbRequests->addfirst(dbRequests, &uuids, sizeof(dbRequest *)); + } + static dbRequest *acnts = NULL; + if (NULL == acnts) + { + static char *szi[] = {"FirstName", "LastName", NULL}; + static char *szo[] = {NULL}; + acnts = xzalloc(sizeof(dbRequest)); + acnts->table = "UserAccounts"; + acnts->inParams = szi; + acnts->outParams = szo; + acnts->where = "FirstName=? and LastName=?"; + dbRequests->addfirst(dbRequests, &acnts, sizeof(dbRequest *)); + } + static dbRequest *auth = NULL; + if (NULL == auth) + { + static char *szi[] = {"UUID", NULL}; + static char *szo[] = {"passwordSalt", "passwordHash", NULL}; + auth = xzalloc(sizeof(dbRequest)); + auth->table = "auth"; + auth->inParams = szi; + auth->outParams = szo; + auth->where = "UUID=?"; + dbRequests->addfirst(dbRequests, &auth, sizeof(dbRequest *)); + } + + Rd->fromDb = FALSE; + +// uuid = Rd->shs.UUID; first = getStrH(Rd->stuff, "firstName"); last = getStrH(Rd->stuff, "lastName"); + + // Special for showing another users details. + if ('\0' != getStrH(Rd->queries, "user")[0]) + uuid = ""; + + char *first = xstrdup(""), *last = xstrdup(""); + + if (NULL != firstName) + { + free(first); + first = xstrdup(firstName); + if (NULL == lastName) + { + char *t = strchr(first, ' '); + +d("accountRead() single name |%s| |%s|", first, last); + if (NULL == t) + t = strchr(first, '+'); + if (NULL != t) + { + *t++ = '\0'; + free(last); + last = xstrdup(t); + } + } + else + { + free(last); + last = xstrdup(lastName); + } + } +d("accountRead() UUID %s, name %s %s", uuid, first, last); + uuid_clear(binuuid); + if ((NULL != uuid) && ('\0' != uuid[0])) + uuid_parse(uuid, binuuid); + if ((NULL != uuid) && ('\0' != uuid[0]) && (!uuid_is_null(binuuid))) + { + char *where = xmprintf("%s/users/%s.lua", scData, uuid); + rt = LuaToHash(Rd, where, "user", tnm, ret, &st, &now, "user"); + + free(where); + dbDoSomething(uuids, FALSE, uuid); + rows = uuids->rows; + } + else + { + + if ('\0' != first[0]) + { + char *where = xmprintf("%s/users/%s_%s.lua", scData, first, last); + rt = LuaToHash(Rd, where, "user", tnm, ret, &st, &now, "user"); + + free(where); + dbDoSomething(acnts, FALSE, first, last); // LEAKY + rows = acnts->rows; + } + } +// else +// { +// bitch(Rd, "Unable to read user record.", "Nothing available to look up a user record with."); +// rt = 1; +// } + + if (0 == rt) + { +T("Found Lua record."); + ret += 1; + Rd->database->putstr(Rd->database, "UserAccounts.FirstName", first); + Rd->database->putstr(Rd->database, "UserAccounts.LastName", last); + Rd->database->putstr(Rd->database, "UserAccounts.Email", getStrH(tnm, "email")); + Rd->database->putstr(Rd->database, "UserAccounts.Created", getStrH(tnm, "created")); + Rd->database->putstr(Rd->database, "UserAccounts.PrincipalID", getStrH(tnm, "UUID")); + Rd->database->putstr(Rd->database, "UserAccounts.UserLevel", getStrH(tnm, "level")); + Rd->database->putstr(Rd->database, "UserAccounts.UserFlags", getStrH(tnm, "flags")); + Rd->database->putstr(Rd->database, "UserAccounts.UserTitle", getStrH(tnm, "title")); + Rd->database->putstr(Rd->database, "UserAccounts.active", getStrH(tnm, "active")); + Rd->database->putstr(Rd->database, "auth.passwordSalt", getStrH(tnm, "passwordSalt")); + Rd->database->putstr(Rd->database, "auth.passwordHash", getStrH(tnm, "passwordHash")); + Rd->stuff-> putstr(Rd->stuff, "linky-hashish", getStrH(tnm, "linky-hashish")); + Rd->database->putstr(Rd->database, "Lua.name", getStrH(tnm, "name")); + Rd->database->putstr(Rd->database, "Lua.DoB", getStrH(tnm, "DoB")); + Rd->database->putstr(Rd->database, "Lua.agree", getStrH(tnm, "agree")); + Rd->database->putstr(Rd->database, "Lua.adult", getStrH(tnm, "adult")); + Rd->database->putstr(Rd->database, "Lua.aboutMe", getStrH(tnm, "aboutMe")); + Rd->database->putstr(Rd->database, "Lua.vouched", getStrH(tnm, "vouched")); + Rd->database->putstr(Rd->database, "Lua.voucher", getStrH(tnm, "voucher")); + } +// else if (rows) + if (rows) + { + rt = rows->rows->size(rows->rows); + if (1 == rt) + { + ret = rt; +T("Found database record."); + dbPull(Rd, "UserAccounts", rows); + + char *name = xmprintf("%s %s", getStrH(Rd->database, "UserAccounts.FirstName"), getStrH(Rd->database, "UserAccounts.LastName")); + + Rd->fromDb = TRUE; + Rd->database->putstr(Rd->database, "Lua.name", name); + free(name); + dbDoSomething(auth, FALSE, getStrH(Rd->database, "UserAccounts.PrincipalID")); // LEAKY + rows = auth->rows; + if (rows) + { + if (1 == rows->rows->size(rows->rows)) + dbPull(Rd, "auth", rows); + else + { + free(rows->fieldNames); + rows->rows->free(rows->rows); + free(rows); + } + } + else + { + free(rows->fieldNames); + rows->rows->free(rows->rows); + free(rows); + } + } + else + { + free(rows->fieldNames); + rows->rows->free(rows->rows); + free(rows); + } + } + else + { + d("No user name or UUID to get an account for."); + } + + if (1 == ret) + { +// TODO - this has to change when we are editing other peoples accounts. + if ('\0' == getStrH(Rd->queries, "user")[0]) + { +// Rd->shs.level = atoi(getStrH(Rd->database, "UserAccounts.UserLevel")); +// TODO - might have to combine first and last here. +// Rd->shs.name = Rd->database->getstr(Rd->database, "Lua.name", true); +// Rd->shs.UUID = Rd->database->getstr(Rd->database, "UserAccounts.PrincipalID", true); +//d("accountRead() setting session uuid %s level %d name %s ", Rd->shs.UUID, (int) Rd->shs.level, Rd->shs.name); + } +// Rd->stuff->putstr(Rd->stuff, "email", getStrH(Rd->database, "UserAccounts.Email")); + } + + free(last); + free(first); + tnm->free(tnm); + return ret; +} + +static int accountDelSub(reqData *Rd, inputForm *iF, inputValue *iV) +{ + int ret = 0; + char *uuid = Rd->shs.UUID, *first = getStrH(Rd->stuff, "firstName"), *last = getStrH(Rd->stuff, "lastName"); + int c = accountRead(Rd, uuid, first, last); + + if (1 != c) + { + bitch(Rd, "Cannot delete account.", "Account doesn't exist."); + ret++; + } + else + { +// check if logged in user is allowed to delete this account +// delete user record +// log the user out if they are logged in + } + return ret; +} + +// The [create member] button on accountLoginWeb() +static int accountCreateSub(reqData *Rd, inputForm *iF, inputValue *iV) +{ + int ret = 0; + char *uuid = Rd->shs.UUID, *first = getStrH(Rd->stuff, "name"), *last = NULL; + int c = accountRead(Rd, uuid, first, last); + + if (strcmp("POST", Rd->Method) == 0) + { + if (0 != c) + { + bitch(Rd, "Cannot create account.", "Account exists."); + Rd->shs.status = SHS_NUKE; + ret++; + } + else + { + char *salt = newSLOSsalt(Rd); + char *h = checkSLOSpassword(Rd, salt, getStrH(Rd->body, "password"), NULL, NULL); + + if (NULL == h) + ret++; + else + { + Rd->stuff->putstr(Rd->stuff, "passHash", h); + Rd->stuff->putstr(Rd->stuff, "passSalt", salt); + if (NULL != Rd->shs.name) free(Rd->shs.name); + // So that we can get the name later when we show the account data entry page via GET. + Rd->shs.name = Rd->stuff->getstr(Rd->stuff, "name", true); + free(h); + Rd->shs.status = SHS_REFRESH; + } + free(salt); + if (0 != ret) + Rd->shs.status = SHS_NUKE; + } + } + return ret; +} + +// The [confirm] button on accountAddWeb() +static int accountAddSub(reqData *Rd, inputForm *iF, inputValue *iV) +{ + int ret = 0; + char *uuid = Rd->shs.UUID, *first = getStrH(Rd->stuff, "firstName"), *last = getStrH(Rd->stuff, "lastName"); + int c = accountRead(Rd, uuid, first, last); + + if (0 != c) + { + bitch(Rd, "Cannot add account.", "Account exists."); + Rd->shs.status = SHS_NUKE; + ret++; + } + else if ((0 == ret) && (strcmp("POST", Rd->Method) == 0)) + { + char *h = checkSLOSpassword(Rd, getStrH(Rd->stuff, "passSalt"), getStrH(Rd->stuff, "password"), getStrH(Rd->stuff, "passHash"), "Passwords are not the same."); + + if (NULL == h) + { + ret++; + Rd->shs.status = SHS_NUKE; + } + else + { + free(h); + generateAccountUUID(Rd); + Rd->stuff->putstr(Rd->stuff, "passwordHash", getStrH(Rd->stuff, "passHash")); + Rd->stuff->putstr(Rd->stuff, "passwordSalt", getStrH(Rd->stuff, "passSalt")); + Rd->shs.level = -200; + Rd->database->putstr(Rd->database, "UserAccounts.UserLevel", "-200"); + // Generate the linky for the email. + newSesh(Rd, TRUE); + accountWrite(Rd); + // log them in + I("Logged on %s %s Level %d %s", Rd->shs.UUID, Rd->shs.name, Rd->shs.level, getLevel(Rd->shs.level)); + Rd->output = "accountView"; + Rd->form = "accountView"; + Rd->doit = "login"; + Rd->shs.status = SHS_LOGIN; + } + } + return ret; +} + +static int accountSaveSub(reqData *Rd, inputForm *iF, inputValue *iV) +{ + int ret = 0; + // Using body[user] here, coz we got to this page via a URL query. + char *uuid = Rd->shs.UUID, *first = getStrH(Rd->body, "user"), *last = NULL; + int c = accountRead(Rd, NULL, first, last); + + if (1 != c) + { + bitch(Rd, "Cannot save account.", "Account doesn't exist."); + ret++; + } + else if ((0 == ret) && (strcmp("POST", Rd->Method) == 0)) + { + Rd->stuff->putstr(Rd->stuff, "email", getStrH(Rd->database, "UserAccounts.Email")); + Rd->stuff->putstr(Rd->stuff, "created", getStrH(Rd->database, "UserAccounts.Created")); + Rd->stuff->putstr(Rd->stuff, "flags", getStrH(Rd->database, "UserAccounts.UserFlags")); + Rd->stuff->putstr(Rd->stuff, "active", getStrH(Rd->database, "UserAccounts.active")); + Rd->stuff->putstr(Rd->stuff, "passwordSalt", getStrH(Rd->database, "auth.passwordSalt")); + Rd->stuff->putstr(Rd->stuff, "passwordHash", getStrH(Rd->database, "auth.passwordHash")); + Rd->stuff->putstr(Rd->stuff, "name", getStrH(Rd->database, "Lua.name")); + Rd->stuff->putstr(Rd->stuff, "DoB", getStrH(Rd->database, "Lua.DoB")); + Rd->stuff->putstr(Rd->stuff, "agree", getStrH(Rd->database, "Lua.agree")); + Rd->stuff->putstr(Rd->stuff, "adult", getStrH(Rd->database, "Lua.adult")); + Rd->stuff->putstr(Rd->stuff, "aboutMe", getStrH(Rd->database, "Lua.aboutMe")); + Rd->stuff->putstr(Rd->stuff, "vouched", getStrH(Rd->database, "Lua.vouched")); + Rd->stuff->putstr(Rd->stuff, "voucher", getStrH(Rd->database, "Lua.voucher")); + + char *lvl = getStrH(Rd->body, "level"); + qlisttbl_obj_t obj; + + memset((void*)&obj, 0, sizeof(obj)); // must be cleared before call + accountLevels->lock(accountLevels); + while(accountLevels->getnext(accountLevels, &obj, NULL, false) == true) + { + if (strcmp(lvl, (char *) obj.data) == 0) + Rd->database->putstr(Rd->database, "UserAccounts.UserLevel", obj.name); + } + accountLevels->unlock(accountLevels); + accountWrite(Rd); + free(Rd->outQuery); + Rd->outQuery = xmprintf("?user=%s+%s", getStrH(Rd->database, "UserAccounts.FirstName"), getStrH(Rd->database, "UserAccounts.LastName")); +// TODO - this isn't being shown. + addStrL(Rd->messages, "Account saved."); + } + return ret; +} + +// The unique validation URL sent in email. +static int accountValidateSub(reqData *Rd, inputForm *iF, inputValue *iV) +{ + int ret = 0; + char *uuid = Rd->shs.UUID, *first = getStrH(Rd->stuff, "firstName"), *last = getStrH(Rd->stuff, "lastName"); + int c = accountRead(Rd, uuid, first, last); + + if (1 != c) + { + bitch(Rd, "Cannot validate account.", "Account doesn't exist."); + ret++; + } + else + { + Rd->stuff->putstr(Rd->stuff, "email", getStrH(Rd->database, "UserAccounts.Email")); + Rd->stuff->putstr(Rd->stuff, "created", getStrH(Rd->database, "UserAccounts.Created")); + Rd->stuff->putstr(Rd->stuff, "flags", getStrH(Rd->database, "UserAccounts.UserFlags")); + Rd->stuff->putstr(Rd->stuff, "active", getStrH(Rd->database, "UserAccounts.active")); + Rd->stuff->putstr(Rd->stuff, "passwordSalt", getStrH(Rd->database, "auth.passwordSalt")); + Rd->stuff->putstr(Rd->stuff, "passwordHash", getStrH(Rd->database, "auth.passwordHash")); + Rd->stuff->putstr(Rd->stuff, "name", getStrH(Rd->database, "Lua.name")); + Rd->stuff->putstr(Rd->stuff, "DoB", getStrH(Rd->database, "Lua.DoB")); + Rd->stuff->putstr(Rd->stuff, "agree", getStrH(Rd->database, "Lua.agree")); + Rd->stuff->putstr(Rd->stuff, "adult", getStrH(Rd->database, "Lua.adult")); + Rd->stuff->putstr(Rd->stuff, "aboutMe", getStrH(Rd->database, "Lua.aboutMe")); + Rd->stuff->putstr(Rd->stuff, "vouched", getStrH(Rd->database, "Lua.vouched")); + Rd->stuff->putstr(Rd->stuff, "voucher", getStrH(Rd->database, "Lua.voucher")); + Rd->shs.level = -100; + Rd->database->putstr(Rd->database, "UserAccounts.UserLevel", "-100"); + accountWrite(Rd); + Rd->doit = "logout"; + Rd->output = "accountLogin"; + Rd->form = "accountLogin"; + Rd->shs.status = SHS_NUKE; + } + return ret; +} + +static int accountViewSub(reqData *Rd, inputForm *iF, inputValue *iV) +{ +// TODO - this has to change when we are editing other peoples accounts. + int ret = 0; + char *uuid = Rd->shs.UUID, *first = getStrH(Rd->stuff, "firstName"), *last = getStrH(Rd->stuff, "lastName"); + int c = accountRead(Rd, uuid, first, last); + +d("Sub accountViewSub() %s %s %s", uuid, first, last); + if (1 != c) + { + bitch(Rd, "Cannot view account.", "Account doesn't exist."); + ret++; + Rd->shs.status = SHS_NUKE; + } + else + { + // Check password on POST if the session user is the same as the shown user, coz this is the page shown on login. + // Also only check on login. + if ((strcmp("POST", Rd->Method) == 0) //&& (strcmp(Rd->shs.UUID, getStrH(Rd->database, "UserAccounts.PrincipalID")) == 0) + && (strcmp("login", Rd->doit) == 0) && (strcmp("accountLogin", Rd->form) == 0)) + { + char *h = checkSLOSpassword(Rd, getStrH(Rd->database, "auth.passwordSalt"), getStrH(Rd->body, "password"), getStrH(Rd->database, "auth.passwordHash"), "Login failed."); + if (NULL == h) + { + ret++; + Rd->shs.status = SHS_NUKE; + } + else + { + Rd->shs.level = atoi(getStrH(Rd->database, "UserAccounts.UserLevel")); + if (NULL != Rd->shs.name) free(Rd->shs.name); + Rd->shs.name = Rd->database->getstr(Rd->database, "Lua.name", true); + if (NULL != Rd->shs.UUID) free(Rd->shs.UUID); + Rd->shs.UUID = Rd->database->getstr(Rd->database, "UserAccounts.PrincipalID", true); + free(h); + I("Logged on %s %s Level %d %s", Rd->shs.UUID, Rd->shs.name, Rd->shs.level, getLevel(Rd->shs.level)); + Rd->shs.status = SHS_LOGIN; + } + } + } + + return ret; +} +static int accountEditSub(reqData *Rd, inputForm *iF, inputValue *iV) +{ + int ret = 0; + char *uuid = Rd->shs.UUID, *first = getStrH(Rd->stuff, "firstName"), *last = getStrH(Rd->stuff, "lastName"); + int c = accountRead(Rd, uuid, first, last); + +d("Sub accountEditSub %s %s %s", uuid, first, last); + if (1 != c) + { + bitch(Rd, "Cannot edit account.", "Account doesn't exist."); + ret++; + } + else + { +// check if logged in user is allowed to make these changes +// update user record + } + return ret; +} + +static int accountExploreSub(reqData *Rd, inputForm *iF, inputValue *iV) +{ + int ret = 0; +// get a list of user records + return ret; +} + +static int accountOutSub(reqData *Rd, inputForm *iF, inputValue *iV) +{ + int ret = 0; + char *uuid = Rd->shs.UUID, *first = getStrH(Rd->stuff, "firstName"), *last = getStrH(Rd->stuff, "lastName"); + int c = accountRead(Rd, uuid, first, last); + + if (1 != c) + { +// bitch(Rd, "Cannot logout account.", "Account doesn't exist."); +// ret++; + } + + Rd->shs.status = SHS_NUKE; + return ret; +} + +/* TODO - instead of searching through all the users, ... + have a bunch of separate folders with symlinks + scData/users/aaproved + scData/users/disabled + scData/users/god + onefang_rejected.lua -> ../uuid.lua + scData/users/newbie + foo_bar.lua -> ../uuid.lua + scData/users/validated + +*/ +typedef struct _RdAndListTbl RdAndListTbl; +struct _RdAndListTbl +{ + reqData *Rd; + qlisttbl_t *list; +}; +static int accountFilterValidated(struct dirtree *node) +{ + if (!node->parent) return DIRTREE_RECURSE | DIRTREE_SHUTUP; + + if (S_ISREG(node->st.st_mode)) + { + struct stat st; + struct timespec now; + RdAndListTbl *rdl = (RdAndListTbl *) node->parent->extra; + qhashtbl_t *tnm = qhashtbl(0, 0); + char *name = node->name; + char *where = xmprintf("%s/users/%s", scData, node->name); + int rt = LuaToHash(rdl->Rd, where, "user", tnm, 0, &st, &now, "user"); + +t("accountFilterValidatedVoucher %s (%s) -> %s -> %s", name, getStrH(tnm, "level"), getStrH(tnm, "name"), getStrH(tnm, "voucher")); + if ((0 == rt) && (strcmp("-100", getStrH(tnm, "level")) == 0)) + rdl->list->put(rdl->list, getStrH(tnm, "name"), &tnm, sizeof(qhashtbl_t *)); + else + tnm->free(tnm); + free(where); + } + return 0; +} +qlisttbl_t *getAccounts(reqData *Rd) +{ + qlisttbl_t *ret = qlisttbl(0); + RdAndListTbl rdl = {Rd, ret}; + char *path = xmprintf("%s/users", scData); + struct dirtree *new = dirtree_add_node(0, path, 0); + + new->extra = (long) &rdl; + dirtree_handle_callback(new, accountFilterValidated); + ret->sort(ret); + free(path); + + return ret; +} +static void accountExploreValidatedVouchersWeb(reqData *Rd, inputForm *oF, inputValue *oV) +{ + qlisttbl_t *list =getAccounts(Rd); + + if (NULL != Rd->shs.name) free(Rd->shs.name); + Rd->shs.name = NULL; + if (NULL != Rd->shs.UUID) free(Rd->shs.UUID); + Rd->shs.UUID = NULL; + Rd->shs.level = -256; + accountWebHeaders(Rd, oF); + accountWebFields(Rd, oF, oV); + + count = list->size(list); + Rd->reply->addstrf(Rd->reply, ""); + list->free(list); + + accountWebSubs(Rd, oF); + accountWebFooter(Rd, oF); +} +static int accountExploreValidatedVoucherSub(reqData *Rd, inputForm *iF, inputValue *iV) +{ + int ret = 0; + return ret; +} + + +qhashtbl_t *accountPages = NULL; +inputForm *newInputForm(char *name, char *title, char *help, inputFormShowFunc web, inputFormShowFunc eWeb) +{ + inputForm *ret = xmalloc(sizeof(inputForm)); + +d("newInputForm(%s)", name); + ret->name = name; ret->title = title; ret->help = help; + ret->web = web; ret->eWeb = eWeb; + ret->fields = qlisttbl(QLISTTBL_THREADSAFE | QLISTTBL_UNIQUE | QLISTTBL_LOOKUPFORWARD); + ret->subs = qhashtbl(0, 0); + accountPages->put(accountPages, ret->name, ret, sizeof(inputForm)); + free(ret); + return accountPages->get(accountPages, name, NULL, false); +} + +inputField *addInputField(inputForm *iF, signed char type, char *name, char *title, char *help, inputFieldValidFunc validate, inputFieldShowFunc web) +{ + inputField *ret = xzalloc(sizeof(inputField)); + +//d("addInputField(%s, %s)", iF->name, name); + ret->name = name; ret->title = title; ret->help = help; + ret->validate = validate; ret->web = web; ret->type = type; + ret->flags = FLD_EDITABLE; + iF->fields->put(iF->fields, ret->name, ret, sizeof(inputField)); + free(ret); + return iF->fields->get(iF->fields, name, NULL, false); +} + +void inputFieldExtra(inputField *ret, signed char flags, short viewLength, short maxLength) +{ + ret->flags = flags; + ret->viewLength = viewLength; ret->maxLength = maxLength; +} + +void addSession(inputForm *iF) +{ + inputField *fld, **flds = xzalloc(3 * sizeof(*flds)); + +//d("addSession(%s)", iF->name); + flds[0] = addInputField(iF, LUA_TSTRING, "hashish", "hashish", "", sessionValidate, sessionWeb); + inputFieldExtra(flds[0], FLD_HIDDEN, 0, 0); + flds[1] = addInputField(iF, LUA_TSTRING, "toke_n_munchie", "toke_n_munchie", "", sessionValidate, sessionWeb); + inputFieldExtra(flds[1], FLD_HIDDEN, 0, 0); + fld = addInputField(iF, LUA_TGROUP, "sessionGroup", "sessionGroup", "", sessionValidate, sessionWeb); + inputFieldExtra(fld, FLD_HIDDEN, 0, 0); + fld->group = flds; + flds[0]->group = flds; + flds[1]->group = flds; +} + +void addEmailFields(inputForm *iF) +{ + inputField *fld, **flds = xzalloc(3 * sizeof(*flds)); + + flds[0] = addInputField(iF, LUA_TEMAIL, "email", "email", NULL, emailValidate, emailWeb); + inputFieldExtra(flds[0], FLD_EDITABLE, 42, 254); + flds[1] = addInputField(iF, LUA_TEMAIL, "emayl", "Re-enter your email, to be sure you got it correct", + "A validation email will be sent to this email address, you will need to click on the link in it to continue your account creation.", emailValidate, emailWeb); + inputFieldExtra(flds[1], FLD_EDITABLE, 42, 254); + fld = addInputField(iF, LUA_TGROUP, "emailGroup", "emailGroup", "", emailValidate, NULL); + inputFieldExtra(fld, FLD_HIDDEN, 0, 0); + fld->group = flds; + flds[0]->group = flds; + flds[1]->group = flds; +} + +void addDoBFields(inputForm *iF) +{ + inputField *fld, **flds = xzalloc(3 * sizeof(*flds)); + + flds[0] = addInputField(iF, LUA_TSTRING, "DoByear", "year", NULL, DoBValidate, DoByWeb); + flds[1] = addInputField(iF, LUA_TSTRING, "DoBmonth", "month", NULL, DoBValidate, DoBmWeb); + fld = addInputField(iF, LUA_TGROUP, "DoBGroup", "DoBGroup", "", DoBValidate, DoBWeb); + inputFieldExtra(fld, FLD_HIDDEN, 0, 0); + fld->group = flds; + flds[0]->group = flds; + flds[1]->group = flds; +} + +void addLegalFields(inputForm *iF) +{ + inputField *fld, **flds = xzalloc(3 * sizeof(*flds)); + + flds[0] = addInputField(iF, LUA_TBOOLEAN, "adult", "I'm allegedly an adult in my country.", NULL, legalValidate, adultWeb); + flds[1] = addInputField(iF, LUA_TBOOLEAN, "agree", "I accept the Terms of Service.", NULL, legalValidate, agreeWeb); + fld = addInputField(iF, LUA_TGROUP, "legalGroup", "legalGroup", "", legalValidate, legalWeb); + inputFieldExtra(fld, FLD_HIDDEN, 0, 0); + fld->group = flds; + flds[0]->group = flds; + flds[1]->group = flds; +} + +inputSub *addSubmit(inputForm *iF, char *name, char *title, char *help, inputSubmitFunc submit, char *output) +{ + inputSub *ret = xmalloc(sizeof(inputSub)); + +//d("addSubmit(%s, %s)", iF->name, name); + ret->name = name; ret->title = title; ret->help = help; ret->submit = submit; ret->outputForm = output; + iF->subs->put(iF->subs, ret->name, ret, sizeof(inputSub)); + free(ret); + return iF->subs->get(iF->subs, name, NULL, false); +} + + +/* There should be some precedence for values overriding values here. + https://www.w3.org/standards/webarch/protocols + "This intro text is boilerplate for the beta release of w3.org." Fucking useless. Pffft + https://www.w3.org/Protocols/ + Still nothing official, though the ENV / HEADER stuff tends to be about the protocol things, and cookies / body / queries are about the data things. + http://docs.gantry.org/gantry4/advanced/setby + Says that query overrides cookies, but that might be just for their platform. + https://framework.zend.com/manual/1.11/en/zend.controller.request.html + Says - "1. GET, 2. POST, 3. COOKIE, 4. SERVER, 5. ENV." + We don't actually get the headers directly, it's all sent via the env. + +URL query Values actually provided by the user in the FORM, and other things. +POST body Values actually provided by the user in the FORM. +cookies + https://stackoverflow.com/questions/4056306/how-to-handle-multiple-cookies-with-the-same-name + +headers includes HTTP_COOKIE and QUERY_STRING +env includes headers and HTTP_COOKIE and QUERY_STRING + +database Since all of the above are for updating the database anyway, this goes on the bottom, overridden by all. + Though be wary of security stuff. + + Sending cookie headers is a special case, multiples can be sent, otherwise headers are singletons, only send one for each name. +*/ +char *sourceTypes[] = +{ + "cookies", + "body", + "queries", + "stuff" +}; + +static int collectFields(reqData *Rd, inputForm *iF, inputValue *iV, int t) +{ + int i = 0, j; + qlisttbl_obj_t obj; + + memset((void*)&obj, 0, sizeof(obj)); // must be cleared before call + iF->fields->lock(iF->fields); + while(iF->fields->getnext(iF->fields, &obj, NULL, false) == true) + { + inputField *fld = (inputField *) obj.data; + +//if (0 > t) +// d("Collecting %d %s - %s", t, iF->name, fld->name); +//else +// d("Collecting %s %s - %s", sourceTypes[t], iF->name, fld->name); + iV[i].field = fld; + if (LUA_TGROUP == fld->type) + { + if (0 >= t) + { + j = 0; + // If it's a group, number the members relative to the group field. + // Assume the members for this group are the previous ones. + while (iV[i].field->group[j]) + { + j++; + iV[i - j].index = j; + } + } + } + else + { + char *vl = NULL; + + switch (t) + { + // We don't get the cookies metadata. + case 0 : vl = Rd->cookies->getstr(Rd->cookies, obj.name, false); break; + case 1 : vl = Rd->body-> getstr(Rd->body, obj.name, false); break; + case 2 : vl = Rd->queries->getstr(Rd->queries, obj.name, false); break; + case 3 : vl = Rd->queries->getstr(Rd->stuff, obj.name, false); break; + default: break; + } + if ((NULL != iV[i].value) && (NULL != vl)) + { + if (strcmp(vl, iV[i].value) != 0) + W("Collected %s value for %s - %s from %s overriding value from %s", sourceTypes[t], iF->name, fld->name, sourceTypes[t], sourceTypes[iV[i].source]); + else + W("Collected %s value for %s - %s from %s same as value from %s", sourceTypes[t], iF->name, fld->name, sourceTypes[t], sourceTypes[iV[i].source]); + } + if (NULL != vl) + { + iV[i].source = t; + iV[i].value = vl; + D("Collected %s value for %s - %s = %s", sourceTypes[t], iF->name, fld->name, vl); + } + } + i++; + } + iF->fields->unlock(iF->fields); + return i; +} + + +void sessionStateEngine(reqData *Rd, char *type) +{ + switch (Rd->shs.status) + { + case SHS_UNKNOWN: d("sessionStateEngine(SHS_UNKNOWN, %s)", type); break; + case SHS_NONE: d("sessionStateEngine(SHS_NONE, %s)", type); break; + case SHS_BOGUS: d("sessionStateEngine(SHS_BOGUS, %s)", type); break; + case SHS_PROBLEM: d("sessionStateEngine(SHS_PROBLEM, %s)", type); break; + case SHS_VALID: d("sessionStateEngine(SHS_VALID, %s)", type); break; + + case SHS_LOGIN: d("sessionStateEngine(SHS_LOGIN, %s)", type); break; + + case SHS_RENEW: d("sessionStateEngine(SHS_RENEW, %s)", type); break; + case SHS_REFRESH: d("sessionStateEngine(SHS_REFRESH, %s)", type); break; + case SHS_IDLE: d("sessionStateEngine(SHS_IDLE, %s)", type); break; + case SHS_ANCIENT: d("sessionStateEngine(SHS_ANCIENT, %s)", type); break; + + case SHS_SECURITY: d("sessionStateEngine(SHS_SECURITY, %s)", type); break; + case SHS_RELOGIN: d("sessionStateEngine(SHS_RELOGIN, %s)", type); break; + + case SHS_KEEP: d("sessionStateEngine(SHS_KEEP, %s)", type); break; + case SHS_WIPE: d("sessionStateEngine(SHS_WIPE, %s)", type); break; + case SHS_NUKE: d("sessionStateEngine(SHS_NUKE, %s)", type); break; + } +} + + +void account_html(char *file, reqData *Rd, HTMLfile *thisFile) +{ + inputForm *iF; + inputField *fld; + inputSub *sub; + boolean isGET = FALSE; + int e = 0, t = 0, i, j; + char *doit = getStrH(Rd->body, "doit"), *form = getStrH(Rd->body, "form"); + + if (NULL == accountLevels) + { + accountLevels = qlisttbl(QLISTTBL_LOOKUPFORWARD | QLISTTBL_THREADSAFE | QLISTTBL_UNIQUE); + accountLevels->putstr(accountLevels, "-256", "disabled"); + accountLevels->putstr(accountLevels, "-200", "newbie"); + accountLevels->putstr(accountLevels, "-100", "validated"); + accountLevels->putstr(accountLevels, "-50", "vouched for"); + accountLevels->putstr(accountLevels, "0", "approved"); // Note that http://opensimulator.org/wiki/Userlevel claims that 1 and above are "GOD_LIKE". + accountLevels->putstr(accountLevels, "200", "god"); + } + + // Check "Origin" header and /or HTTP_REFERER header. + // "Origin" is either HTTP_HOST or X-FORWARDED-HOST. Which could be "null". + char *ref = xmprintf("https://%s%s/account.html", getStrH(Rd->headers, "SERVER_NAME"), getStrH(Rd->headers, "SCRIPT_NAME")); + char *href = Rd->headers->getstr(Rd->headers, "HTTP_REFERER", true); + + if (NULL != href) + { + char *f = strchr(href, '?'); + + if (NULL != f) + *f = '\0'; + if (('\0' != href[0]) && (strcmp(ref, href) != 0)) + { + bitch(Rd, "Invalid referer.", ref); + D("Invalid referer - %s isn't %s", ref, href); + form = "accountLogin"; + Rd->shs.status = SHS_PROBLEM; + } + free(href); + } + free(ref); + ref = getStrH(Rd->headers, "SERVER_NAME"); + href = getStrH(Rd->headers, "HTTP_HOST"); + if ('\0' == href[0]) + href = getStrH(Rd->headers, "X-FORWARDED-HOST"); + if (('\0' != href[0]) && (strcmp(ref, href) != 0)) + { + bitch(Rd, "Invalid HOST.", ref); + D("Invalid HOST - %s isn't %s", ref, href); + form = "accountLogin"; + Rd->shs.status = SHS_PROBLEM; + } + + // Redirect to HTTPS if it's HTTP. + if (strcmp("https", Rd->Scheme) != 0) + { + Rd->Rheaders->putstr (Rd->Rheaders, "Status", "301 Moved Permanently"); + Rd->Rheaders->putstrf(Rd->Rheaders, "Location", "https://%s%s", Rd->Host, Rd->RUri); + Rd->reply->addstrf(Rd->reply, "
Validated users \n"); + Rd->reply->addstr(Rd->reply, ""); + Rd->reply->addstr(Rd->reply, " \nname "); + Rd->reply->addstr(Rd->reply, "voucher "); + Rd->reply->addstr(Rd->reply, "level "); + Rd->reply->addstr(Rd->reply, "title "); + Rd->reply->addstr(Rd->reply, ""); + + qlisttbl_obj_t obj; + memset((void *) &obj, 0, sizeof(obj)); + list->lock(list); + while(list->getnext(list, &obj, NULL, false) == true) + { + qhashtbl_t *tnm = *((qhashtbl_t **) obj.data); + char *nm = qstrreplace("tr", xstrdup(obj.name), " ", "+"); + + Rd->reply->addstrf(Rd->reply, " ", getStrH(tnm, "voucher"), getStrH(tnm, "level"), getStrH(tnm, "title")); + free(nm); + tnm->clear(tnm); + list->removeobj(list, &obj); + tnm->free(tnm); + } + list->unlock(list); + Rd->reply->addstr(Rd->reply, " %s ", Rd->Host, Rd->Script, Rd->Path, nm, obj.name); + Rd->reply->addstrf(Rd->reply, "%s %s %s 404 Unknown page " + "" + "You should get redirected to https://%s%s", + Rd->Host, Rd->RUri, Rd->Host, Rd->RUri, Rd->Host, Rd->RUri + ); + D("Redirecting dynamic page %s -> https://%s%s", file, Rd->Host, Rd->RUri); + return; + } + + // Create the dynamic web pages for account.html. + if (NULL == accountPages) + { + accountPages = qhashtbl(0, 0); + + + iF = newInputForm("accountAdd", "", NULL, accountAddWeb, accountLoginWeb); + addSession(iF); +// fld = addInputField(iF, LUA_TSTRING, "UUID", "UUID", NULL, UUIDValidate, UUIDWeb); +// inputFieldExtra(fld, FLD_HIDDEN, 0, 0); + fld = addInputField(iF, LUA_TSTRING, "name", "name", NULL, nameValidate, nameWeb); + inputFieldExtra(fld, FLD_EDITABLE | FLD_REQUIRED, 42, 63); + fld = addInputField(iF, LUA_TPASSWORD, "psswrd", "Re-enter your password", + "Warning, the limit on password length is set by your viewer, some can't handle longer than 16 characters.", passwordValidate, passwordWeb); + inputFieldExtra(fld, FLD_EDITABLE, 16, 0); + addEmailFields(iF); + addDoBFields(iF); + addLegalFields(iF); + fld = addInputField(iF, LUA_TSTRING, "ToS", "Terms of Service", "", NULL, ToSWeb); + fld = addInputField(iF, LUA_TSTRING, "voucher", "The grid name of someone that will vouch for you", + "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); + inputFieldExtra(fld, FLD_EDITABLE, 42, 63); + fld = addInputField(iF, LUA_TSTRING, "aboutMe", "About me", NULL, aboutMeValidate, aboutMeWeb); + inputFieldExtra(fld, FLD_EDITABLE, 50, 16384); + addSubmit(iF, "confirm", "confirm", NULL, accountAddSub, "accountView"); + addSubmit(iF, "cancel", "cancel", NULL, accountOutSub, "accountLogin"); + + + iF = newInputForm("accountView", "account view", NULL, accountViewWeb, accountLoginWeb); + addSession(iF); +// fld = addInputField(iF, LUA_TSTRING, "UUID", "UUID", NULL, UUIDValidate, UUIDWeb); +// inputFieldExtra(fld, FLD_HIDDEN, 0, 0); + fld = addInputField(iF, LUA_TSTRING, "name", "name", NULL, nameValidate, nameWeb); + inputFieldExtra(fld, FLD_HIDDEN, 42, 63); + fld = addInputField(iF, LUA_TSTRING, "user", "user", NULL, nameValidate, nameWeb); + inputFieldExtra(fld, FLD_HIDDEN, 42, 63); + addSubmit(iF, "login", "", NULL, accountViewSub, "accountView"); // Coz we sometimes want to trigger this from code. + addSubmit(iF, "validate", "", NULL, accountValidateSub, "accountLogin"); // Coz we sometimes want to trigger this from code. + addSubmit(iF, "edit", "", NULL, accountEditSub, "accountEdit"); // Coz we sometimes want to trigger this from code. + addSubmit(iF, "validated_members", "validated members", NULL, accountExploreValidatedVoucherSub, "accountValidated"); + addSubmit(iF, "logout", "logout", NULL, accountOutSub, "accountLogin"); + + + iF = newInputForm("accountValidated", "account validated list", NULL, accountExploreValidatedVouchersWeb, accountLoginWeb); + addSession(iF); + fld = addInputField(iF, LUA_TSTRING, "name", "name", NULL, nameValidate, nameWeb); + inputFieldExtra(fld, FLD_HIDDEN, 42, 63); + addSubmit(iF, "login", "", NULL, accountViewSub, "accountView"); // Coz we sometimes want to trigger this from code. + addSubmit(iF, "back", "back", NULL, accountViewSub, "accountView"); + + + iF = newInputForm("accountEdit", "account edit", NULL, accountEditWeb, accountLoginWeb); + addSession(iF); +// fld = addInputField(iF, LUA_TSTRING, "UUID", "UUID", NULL, UUIDValidate, UUIDWeb); +// inputFieldExtra(fld, FLD_HIDDEN, 0, 0); +// fld = addInputField(iF, LUA_TSTRING, "name", "name", NULL, nameValidate, nameWeb); +// inputFieldExtra(fld, FLD_HIDDEN, 42, 63); +// fld = addInputField(iF, LUA_TSTRING, "user", "user", NULL, nameValidate, nameWeb); +// inputFieldExtra(fld, FLD_HIDDEN, 42, 63); +// fld = addInputField(iF, LUA_TEMAIL, "email", "email", "", emailValidate, emailWeb); +// inputFieldExtra(fld, FLD_NONE, 42, 254); + addSubmit(iF, "login", "", NULL, accountViewSub, "accountView"); // Coz we sometimes want to trigger this from code. + addSubmit(iF, "save", "save", NULL, accountSaveSub, "accountView"); + addSubmit(iF, "back", "back", NULL, accountViewSub, "accountView"); +// addSubmit(iF, "members", "members", NULL, accountExploreSub, "accountExplore"); + addSubmit(iF, "logout", "logout", NULL, accountOutSub, "accountLogin"); +// addSubmit(iF, "delete", "delete", NULL, accountDelSub, "accountDel"); + + + iF = newInputForm("accountLogin", "account login", "Please login, or create your new account.", accountLoginWeb, accountLoginWeb); + addSession(iF); + fld = addInputField(iF, LUA_TSTRING, "name", "name", "Your name needs to be two words, only ordinary letters and digits, no special characters or fonts.", nameValidate, nameWeb); + inputFieldExtra(fld, FLD_EDITABLE | FLD_REQUIRED, 42, 63); + fld = addInputField(iF, LUA_TPASSWORD, "password", "password", + "Warning, the limit on password length is set by your viewer, some can't handle longer than 16 characters.", passwordValidate, passwordWeb); + inputFieldExtra(fld, FLD_EDITABLE | FLD_REQUIRED, 16, 0); + addSubmit(iF, "logout", "", NULL, accountOutSub, "accountLogin"); // Coz we sometimes want to trigger this from code. + addSubmit(iF, "validate", "", NULL, accountValidateSub, "accountLogin"); // Coz we sometimes want to trigger this from code. + addSubmit(iF, "login", "login", NULL, accountViewSub, "accountView"); + addSubmit(iF, "create", "create account", NULL, accountCreateSub, "accountAdd"); + } + + // Figure out what we are doing. + if ('\0' == form[0]) + form = getStrH(Rd->cookies, "form"); + if ('\0' == form[0]) + { + form = "accountLogin"; + Rd->shs.status = SHS_NUKE; + } + if ('\0' == doit[0]) + doit = getStrH(Rd->cookies, "doit"); + if ('\0' == doit[0]) + { + doit = "logout"; + Rd->shs.status = SHS_NUKE; + } + if ('\0' != doit[0]) + { + setCookie(Rd, "doit", doit); + Rd->doit = doit; + } + + iF = accountPages->get(accountPages, form, NULL, false); + if (NULL == iF) + { + E("No such account page - %s", form); + form = "accountLogin"; + doit = "logout"; + Rd->shs.status = SHS_PROBLEM; + iF = accountPages->get(accountPages, form, NULL, false); + } + sub = iF->subs->get(iF->subs, doit, NULL, false); + if (NULL == sub) + { + E("No such account action - %s", doit); + form = "accountLogin"; + doit = "logout"; + Rd->shs.status = SHS_PROBLEM; + iF = accountPages->get(accountPages, form, NULL, false); + sub = iF->subs->get(iF->subs, doit, NULL, false); + } + + // Special for showing a users details. + if ('\0' != getStrH(Rd->queries, "user")[0]) + { + doit = "edit"; + form = "accountView"; + iF = accountPages->get(accountPages, form, NULL, false); + sub = iF->subs->get(iF->subs, doit, NULL, false); + } + + Rd->doit = doit; + Rd->form = form; + Rd->output = sub->outputForm; + + C("Starting dynamic page %s %s -> %s [%s -> %s]", Rd->RUri, form, doit, sub->name, Rd->output); + + // Collect the input data. + int count = iF->fields->size(iF->fields); + inputValue *iV = xzalloc(count * sizeof(inputValue)); + qlisttbl_obj_t obj; + + sessionStateEngine(Rd, "collected"); + + if (strcmp("cancel", sub->name) != 0) + { + +// TODO - complain about any extra body or query parameter that we where not expecting. + for (t = 0; t < 3; t++) + i = collectFields(Rd, iF, iV, t); + + // Validate the input data. + D("For page %s -> %s, start of validation.", iF->name, sub->name); + t = i; + for (i = 0; i < t; i++) + { + if ((NULL != iV[i].value) || (LUA_TGROUP == iV[i].field->type)) + { + if (NULL == iV[i].field->validate) + E("No validation function for %s - %s", iF->name, iV[i].field->name); + else + { + if (0 == iV[i].valid) + { + D("Validating %s - %s", iF->name, iV[i].field->name); + int rt = iV[i].field->validate(Rd, iF, &iV[i]); + if (rt) + { + W("Invalidated %s - %s returned %d errors", iF->name, iV[i].field->name, rt); + iV[i].valid = -1; + } + else + { + D("Validated %s - %s", iF->name, iV[i].field->name); + iV[i].valid = 1; + } + e += rt; + if (NULL != iV[i].field->group) + { + // Use the indexes to set the validation result for the other members of the group. + // The assumption is that the validation functions for each are the same, and it validates the members as a group. + for (j = iV[i].index; j > 0; j--) + iV[i + j].valid = iV[i].valid; +// TODO - Possible off by one error, but I don't think it matters. Needs more testing. + for (j = 0; j <= iV[i].index; j++) + iV[i + j].valid = iV[i].valid; + } + } + else if (0 > iV[i].valid) + D("Already invalidated %s - %s", iF->name, iV[i].field->name); + else if (0 < iV[i].valid) + D("Already validated %s - %s", iF->name, iV[i].field->name); + } + } + doit = Rd->doit; + form = Rd->form; + } + + sessionStateEngine(Rd, "validated"); + + // Submit the data. Reload input form and sub in case things got changed by the validation functions. + iF = accountPages->get(accountPages, Rd->form, NULL, false); + sub = iF->subs->get(iF->subs, Rd->doit, NULL, false); +//if (strcmp("POST", Rd->Method) == 0) // Not such a good idea, since we are faking a submit for account validation, which is a GET. +{ + if (0 == e) + { + D("For page %s, start of %s submission.", iF->name, sub->name); + if (NULL != sub->submit) + e += sub->submit(Rd, iF, iV); + } +} + free(iV); + + } + else + { + sessionStateEngine(Rd, "CANCELLED"); + } + + sessionStateEngine(Rd, "submited"); + switch (Rd->shs.status) + { + case SHS_RENEW: + case SHS_REFRESH: + { + freeSesh(Rd, FALSE, FALSE); + newSesh(Rd, FALSE); + break; + } + + case SHS_VALID: + case SHS_KEEP: + { + // Do nothing here. + break; + } + + case SHS_LOGIN: + case SHS_WIPE: + { +// TODO - should wipe the old one, and create this new one with the users UUID. +// I think that's what we are doing anyway. + freeSesh(Rd, FALSE, FALSE); + newSesh(Rd, FALSE); + break; + } + +// 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. + case SHS_IDLE: + case SHS_SECURITY: + case SHS_RELOGIN: + + case SHS_UNKNOWN: + case SHS_NONE: + case SHS_BOGUS: + case SHS_PROBLEM: + case SHS_ANCIENT: + case SHS_NUKE: + { + freeSesh(Rd, FALSE, TRUE); // Wipe mode clears out all of Rd->database, selected Rd->stuff, and the above commented out Rd->shs. + newSesh(Rd, FALSE); + form = "accountLogin"; + doit = "logout"; + Rd->doit = doit; + Rd->form = form; + iF = accountPages->get(accountPages, Rd->form, NULL, false); + sub = iF->subs->get(iF->subs, Rd->doit, NULL, false); + Rd->output = sub->outputForm; + break; + } + } + + // Return the result. + if (0 == e) + { + if (strcmp("GET", Rd->Method) == 0) + { + // Find the output form. + inputForm *oF = accountPages->get(accountPages, Rd->output, NULL, false); + if (NULL == oF) + { + E("No such account page - %s", Rd->output); + form = "accountLogin"; + doit = "logout"; + oF = accountPages->get(accountPages, form, NULL, false); + } + D("Building output page %s", oF->name); + count = oF->fields->size(oF->fields); + iV = xzalloc(count * sizeof(inputValue)); + collectFields(Rd, oF, iV, -1); + collectFields(Rd, oF, iV, 3); + oF->web(Rd, oF, iV); + free(iV); + } + else + { + // Redirect to a GET if it was a POST. + // The reason here is to avoid a refresh turning into a rePOST. + if ((strcmp("POST", Rd->Method) == 0)) + { + if ('\0' != Rd->form[0]) + setCookie(Rd, "form", Rd->form); + if ('\0' != Rd->doit[0]) + setCookie(Rd, "doit", Rd->doit); + Rd->Rheaders->putstr (Rd->Rheaders, "Status", "303 See Other"); + Rd->Rheaders->putstrf(Rd->Rheaders, "Location", "https://%s%s%s", Rd->Host, Rd->RUri, Rd->outQuery); + Rd->reply->addstrf(Rd->reply, "Post POST redirect " + "" + "You should get redirected to https://%s%s%s", + Rd->Host, Rd->RUri, Rd->outQuery, Rd->Host, Rd->RUri, Rd->outQuery, Rd->Host, Rd->RUri, Rd->outQuery + ); + I("Redirecting dynamic page %s -> https://%s%s%s (%s)", file, Rd->Host, Rd->RUri, Rd->outQuery, Rd->form); + } + } + } + else + { + if (0 < e) + E("Building output ERROR page %s, coz of %d errors.", iF->name, e); + else + D("Building alternate output page %s", iF->name); + // First just sort out input groups, then get the data from Rd->stuff. + count = iF->fields->size(iF->fields); + iV = xzalloc(count * sizeof(inputValue)); + collectFields(Rd, iF, iV, -1); + collectFields(Rd, iF, iV, 3); + iF->eWeb(Rd, iF, iV); + free(iV); + } + + free(Rd->outQuery); + Rd->outQuery = NULL; + + C("Ending dynamic page %s %s", Rd->RUri, form); +} + + +static void cleanup(void) +{ +// TODO - not sure why, but this gets called twice on quitting sometimes. + C("Caught signal, or quitting, cleaning up."); + + if (accountPages) + { + qhashtbl_obj_t obj; + + memset((void*)&obj, 0, sizeof(obj)); + accountPages->lock(accountPages); + while(accountPages->getnext(accountPages, &obj, false) == true) + { + inputForm *f = (inputForm *) obj.data; + + f->subs->free(f->subs); + qlisttbl_obj_t fobj; + + memset((void *) &fobj, 0, sizeof(fobj)); + f->fields->lock(f->fields); + while(f->fields->getnext(f->fields, &fobj, NULL, false) == true) + { + inputField *fld = (inputField *) fobj.data; + + if (LUA_TGROUP == fld->type) + free(fld->group); + } + f->fields->unlock(f->fields); + f->fields->free(f->fields); + } + accountPages->unlock(accountPages); + accountPages->free(accountPages); + accountPages = NULL; + } + + if (dynPages) dynPages->free(dynPages); + dynPages = NULL; + if (HTMLfileCache) + { + qhashtbl_obj_t obj; + + memset((void*)&obj, 0, sizeof(obj)); + HTMLfileCache->lock(HTMLfileCache); + while(HTMLfileCache->getnext(HTMLfileCache, &obj, false) == true) + { + HTMLfile *f = (HTMLfile *) obj.data; + + unfragize(f->fragments, NULL, TRUE); + } + HTMLfileCache->unlock(HTMLfileCache); + HTMLfileCache->free(HTMLfileCache); + HTMLfileCache = NULL; + } + if (mimeTypes) mimeTypes->free(mimeTypes); + mimeTypes = NULL; + freeDb(true); + if (L) lua_close(L); + L = NULL; + if (stats) + { + if (stats->stats) stats->stats->free(stats->stats); + free(stats); + stats = NULL; + } + if (configs) configs->free(configs); + configs = NULL; +} + + +void sledjchisl_main(void) +{ + char *cmd = *toys.optargs; + char *tmp; + struct stat statbuf; + int status, result, i; + void *vd; + + configs = qhashtbl(0, 0); + L = luaL_newstate(); + + I("libfcgi version: %s", FCGI_VERSION); + I("Lua version: %s", LUA_RELEASE); + I("LuaJIT version: %s", LUAJIT_VERSION); + I("MariaDB / MySQL client version: %s", mysql_get_client_info()); + I("OpenSSL version: %s", OPENSSL_VERSION_TEXT); + I("qLibc version: qLibc only git tags for version numbers. Sooo, 2.4.4, unless I forgot to update this."); + I("toybox version: %s", TOYBOX_VERSION); + + dbRequests = qlist(0); + sigatexit(cleanup); + + pwd = getcwd(0, 0); + + if (-1 == fstat(STDIN_FILENO, &statbuf)) + { + error_msg("fstat() failed"); + if (1 != isatty(STDIN_FILENO)) + isWeb = TRUE; + } + else + { + if (S_ISREG (statbuf.st_mode)) D("STDIN is a regular file."); + else if (S_ISDIR (statbuf.st_mode)) D("STDIN is a directory."); + else if (S_ISCHR (statbuf.st_mode)) D("STDIN is a character device."); + else if (S_ISBLK (statbuf.st_mode)) D("STDIN is a block device."); + else if (S_ISFIFO(statbuf.st_mode)) D("STDIN is a FIFO (named pipe)."); + else if (S_ISLNK (statbuf.st_mode)) D("STDIN is a symbolic link."); + else if (S_ISSOCK(statbuf.st_mode)) D("STDIN is a socket."); + else D("STDIN is a unknown file descriptor type."); + if (!S_ISCHR(statbuf.st_mode)) + isWeb = TRUE; + } + + // Print our user name and groups. + struct passwd *pw; + struct group *grp; + uid_t euid = geteuid(); + gid_t egid = getegid(); + gid_t *groups = (gid_t *)toybuf; + i = sizeof(toybuf)/sizeof(gid_t); + int ngroups; + + pw = xgetpwuid(euid); + I("Running as user %s", pw->pw_name); + + grp = xgetgrgid(egid); + ngroups = getgroups(i, groups); + if (ngroups < 0) perror_exit("getgroups"); + D("User is in group %s", grp->gr_name); + for (i = 0; i < ngroups; i++) { + if (groups[i] != egid) + { + if ((grp = getgrgid(groups[i]))) + D("User is in group %s", grp->gr_name); + else + D("User is in group %u", groups[i]); + } + } + + +/* From http://luajit.org/install.html - +To change or extend the list of standard libraries to load, copy +src/lib_init.c to your project and modify it accordingly. Make sure the +jit library is loaded or the JIT compiler will not be activated. +*/ + luaL_openlibs(L); // Load Lua libraries. + + // Load the config scripts. + char *cPaths[] = + { + ".sledjChisl.conf.lua", + "/etc/sledjChisl.conf.lua", +// "/etc/sledjChisl.d/*.lua", + "~/.sledjChisl.conf.lua", +// "~/.config/sledjChisl/*.lua", + NULL + }; + struct stat st; + + + for (i = 0; cPaths[i]; i++) + { + memset(toybuf, 0, sizeof(toybuf)); + if (('/' == cPaths[i][0]) || ('~' == cPaths[i][0])) + snprintf(toybuf, sizeof(toybuf), "%s", cPaths[i]); + else + snprintf(toybuf, sizeof(toybuf), "%s/%s", pwd, cPaths[i]); + if (0 != lstat(toybuf, &st)) + continue; + I("Loading configuration file - %s", toybuf); + status = luaL_loadfile(L, toybuf); + if (status) // If something went wrong, error message is at the top of the stack. + E("Couldn't load file: %s", lua_tostring(L, -1)); + else + { + result = lua_pcall(L, 0, LUA_MULTRET, 0); + if (result) + E("Failed to run script: %s", lua_tostring(L, -1)); + else + { + lua_getglobal(L, "config"); + lua_pushnil(L); + while(lua_next(L, -2) != 0) + { + char *n = (char *) lua_tostring(L, -2); + + // Numbers can convert to strings, so check for numbers before checking for strings. + // On the other hand, strings that can be converted to numbers also pass lua_isnumber(). sigh + if (lua_isnumber(L, -1)) + { + float v = lua_tonumber(L, -1); + configs->put(configs, n, &v, sizeof(float)); + } + else if (lua_isstring(L, -1)) + configs->putstr(configs, n, (char *) lua_tostring(L, -1)); + else if (lua_isboolean(L, -1)) + { + int v = lua_toboolean(L, -1); + configs->putint(configs, n, v); + } + else + { + char *v = (char *) lua_tostring(L, -1); + E("Unknown config variable type for %s = %s", n, v); + } + lua_pop(L, 1); + } + } + } + } + DEBUG = configs->getint(configs, "debug"); + D("Setting DEBUG = %d", DEBUG); + if ((vd = configs->get (configs, "loadAverageInc", NULL, false)) != NULL) {loadAverageInc = *((float *) vd); D("Setting loadAverageInc = %f", loadAverageInc);} + if ((vd = configs->get (configs, "simTimeOut", NULL, false)) != NULL) {simTimeOut = (int) *((float *) vd); D("Setting simTimeOut = %d", simTimeOut);} + if ((tmp = configs->getstr(configs, "scRoot", false)) != NULL) {scRoot = tmp; D("Setting scRoot = %s", scRoot);} + if ((tmp = configs->getstr(configs, "scUser", false)) != NULL) {scUser = tmp; D("Setting scUser = %s", scUser);} + if ((tmp = configs->getstr(configs, "Tconsole", false)) != NULL) {Tconsole = tmp; D("Setting Tconsole = %s", Tconsole);} + if ((tmp = configs->getstr(configs, "Tsocket", false)) != NULL) {Tsocket = tmp; D("Setting Tsocket = %s", Tsocket);} + if ((tmp = configs->getstr(configs, "Ttab", false)) != NULL) {Ttab = tmp; D("Setting Ttab = %s", Ttab);} + if ((tmp = configs->getstr(configs, "webRoot", false)) != NULL) {webRoot = tmp; D("Setting webRoot = %s", webRoot);} + if ((tmp = configs->getstr(configs, "URL", false)) != NULL) {URL = tmp; D("Setting URL = %s", URL);} + if ((vd = configs->get (configs, "seshRenew", NULL, false)) != NULL) {seshRenew = (int) *((float *) vd); D("Setting seshRenew = %d", seshRenew);} + if ((vd = configs->get (configs, "idleTimeOut", NULL, false)) != NULL) {idleTimeOut = (int) *((float *) vd); D("Setting idleTimeOut = %d", idleTimeOut);} + if ((vd = configs->get (configs, "seshTimeOut", NULL, false)) != NULL) {seshTimeOut = (int) *((float *) vd); D("Setting seshTimeOut = %d", seshTimeOut);} + if ((vd = configs->get (configs, "newbieTimeOut", NULL, false)) != NULL) {newbieTimeOut = (int) *((float *) vd); D("Setting newbieTimeOut = %d", newbieTimeOut);} + if ((tmp = configs->getstr(configs, "ToS", false)) != NULL) {ToS = tmp; D("Setting ToS = %s", ToS);} + if ((tmp = configs->getstr(configs, "webIframers", false)) != NULL) {webIframers = tmp; D("Setting webIframers = %s", webIframers);} + + + // Use a FHS compatible setup - + if (strcmp("/", scRoot) == 0) + { + scBin = "/bin"; + scEtc = "/etc/opensim_SC"; + scLib = "/usr/lib/opensim_SC"; + scRun = "/run/opensim_SC"; + scBackup = "/var/backups/opensim_SC"; + scCache = "/var/cache/opensim_SC"; + scData = "/var/lib/opensim_SC"; + scLog = "/var/log/opensim_SC"; + } + else if (strcmp("/usr/local", scRoot) == 0) + { + scBin = "/usr/local/bin"; + scEtc = "/usr/local/etc/opensim_SC"; + scLib = "/usr/local/lib/opensim_SC"; + scRun = "/run/opensim_SC"; + scBackup = "/var/local/backups/opensim_SC"; + scCache = "/var/local/cache/opensim_SC"; + scData = "/var/local/lib/opensim_SC"; + scLog = "/var/local/log/opensim_SC"; + } + else // A place for everything to live, like /opt/opensim_SC + { + char *slsh = ""; + + if ('/' != scRoot[0]) + { + E("scRoot is not an absolute path - %s.", scRoot); + slsh = "/"; + } + // The problem here is that OpenSim already uses /opt/opensim_SC/current/bin for all of it's crap, where they mix everything together. + // Don't want it in the path. + // Could put the .dlls into lib. Most of the rest is config files or common assets. + // Or just slowly migrate opensim stuff to FHS. + scBin = xmprintf("%s%s/bin", slsh, scRoot); + scEtc = xmprintf("%s%s/etc", slsh, scRoot); + scLib = xmprintf("%s%s/lib", slsh, scRoot); + scRun = xmprintf("%s%s/var/run", slsh, scRoot); + scBackup = xmprintf("%s%s/var/backups", slsh, scRoot); + scCache = xmprintf("%s%s/var/cache", slsh, scRoot); + scData = xmprintf("%s%s/var/lib", slsh, scRoot); + scLog = xmprintf("%s%s/var/log", slsh, scRoot); + } + + +// TODO - still a bit chicken and egg here about the tmux socket and reading configs from scEtc /.sledjChisl.conf.lua + if (!isWeb) + { + I("Outputting to a terminal, not a web server."); + // Check if we are already running inside the proper tmux server. + char *eTMUX = getenv("TMUX"); + memset(toybuf, 0, sizeof(toybuf)); + snprintf(toybuf, sizeof(toybuf), "%s/%s", scRun, Tsocket); + if (((eTMUX) && (0 == strncmp(toybuf, eTMUX, strlen(toybuf))))) + { + I("Running inside the proper tmux server. %s", eTMUX); + isTmux = TRUE; + } + else + I("Not running inside the proper tmux server, starting it. %s == %s", eTMUX, toybuf); + } + + if (isTmux || isWeb) + { + char *d; + + // Doing this here coz at this point we should be the correct user. + if ((! qfile_exist(scBin)) && (! qfile_mkdir(scBin, S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP, true))) C("Unable to create path %s", scBin); + if ((! qfile_exist(scEtc)) && (! qfile_mkdir(scEtc, S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP, true))) C("Unable to create path %s", scEtc); + if ((! qfile_exist(scLib)) && (! qfile_mkdir(scLib, S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP, true))) C("Unable to create path %s", scLib); + if ((! qfile_exist(scRun)) && (! qfile_mkdir(scRun, S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IWGRP | S_IXGRP | S_ISGID, true))) C("Unable to create path %s", scRun); + if ((! qfile_exist(scBackup)) && (! qfile_mkdir(scBackup, S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP, true))) C("Unable to create path %s", scBackup); +// TODO - the path to scCache/sledjchisl.socket needs to be readable by the www-data group. So the FCGI socket will work. +// AND it needs to be group sticky on opensimsc group. So the tmux socket will work. +// So currently scCache is www-data readable, and scRun is group sticky. + if ((! qfile_exist(scCache)) && (! qfile_mkdir(scCache, S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP, true))) C("Unable to create path %s", scCache); + if ((! qfile_exist(scData)) && (! qfile_mkdir(scData, S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP, true))) C("Unable to create path %s", scData); + if ((! qfile_exist(scLog)) && (! qfile_mkdir(scLog, S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP, true))) C("Unable to create path %s", scLog); + tmp = xmprintf("%s/sessions", scCache); + if ((! qfile_exist(tmp)) && (! qfile_mkdir(tmp, S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP, true))) C("Unable to create path %s", tmp); + free(tmp); + tmp = xmprintf("%s/users", scData); + if ((! qfile_exist(tmp)) && (! qfile_mkdir(tmp, S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP, true))) C("Unable to create path %s", tmp); + free(tmp); + + + mimeTypes = qhashtbl(0, 0); + mimeTypes->putstr(mimeTypes, "gz", "application/gzip"); + mimeTypes->putstr(mimeTypes, "js", "application/javascript"); + mimeTypes->putstr(mimeTypes, "json", "application/json"); + mimeTypes->putstr(mimeTypes, "pdf", "application/pdf"); + mimeTypes->putstr(mimeTypes, "rtf", "application/rtf"); + mimeTypes->putstr(mimeTypes, "zip", "application/zip"); + mimeTypes->putstr(mimeTypes, "xz", "application/x-xz"); + mimeTypes->putstr(mimeTypes, "gif", "image/gif"); + mimeTypes->putstr(mimeTypes, "png", "image/png"); + mimeTypes->putstr(mimeTypes, "jp2", "image/jp2"); + mimeTypes->putstr(mimeTypes, "jpg2", "image/jp2"); + mimeTypes->putstr(mimeTypes, "jpe", "image/jpeg"); + mimeTypes->putstr(mimeTypes, "jpg", "image/jpeg"); + mimeTypes->putstr(mimeTypes, "jpeg", "image/jpeg"); + mimeTypes->putstr(mimeTypes, "svg", "image/svg+xml"); + mimeTypes->putstr(mimeTypes, "svgz", "image/svg+xml"); + mimeTypes->putstr(mimeTypes, "tif", "image/tiff"); + mimeTypes->putstr(mimeTypes, "tiff", "image/tiff"); + mimeTypes->putstr(mimeTypes, "css", "text/css"); + mimeTypes->putstr(mimeTypes, "html", "text/html"); + mimeTypes->putstr(mimeTypes, "htm", "text/html"); + mimeTypes->putstr(mimeTypes, "shtml", "text/html"); +// mimeTypes->putstr(mimeTypes, "md", "text/markdown"); +// mimeTypes->putstr(mimeTypes, "markdown", "text/markdown"); + mimeTypes->putstr(mimeTypes, "txt", "text/plain"); + + memset(toybuf, 0, sizeof(toybuf)); + snprintf(toybuf, sizeof(toybuf), "%s/config/config.ini", scRoot); + +// TODO - it looks like OpenSim invented their own half arsed backwards INI file include system. +// I doubt qLibc supports it, like it supports what seems to be the standard include system. +// Not sure if we need to worry about it just yet. +// TODO - this leaks memory, but it's a bug in qLibc. Send the bug fix upstream. + qlisttbl_t *qconfig = qconfig_parse_file(NULL, toybuf, '='); + if (NULL == qconfig) + { + E("Can't read config file %s", toybuf); + goto finished; + } + d = qstrunchar(qconfig->getstr(qconfig, "Const.ConnectionString", false), '"', '"'); + + if (NULL == d) + { + E("No database credentials in %s!", toybuf); + goto finished; + } + else + { + char *p0, *p1, *p2; + if (NULL == (d = strdup(d))) + { + E("Out of memory!"); + goto finished; + } + // Data Source=MYSQL_HOST;Database=MYSQL_DB;User ID=MYSQL_USER;Password=MYSQL_PASSWORD;Old Guids=true; + p0 = d; + while (NULL != p0) + { + p1 = strchr(p0, '='); + if (NULL == p1) break; + *p1 = '\0'; + p2 = strchr(p1 + 1, ';'); + if (NULL == p2) break; + *p2 = '\0'; + configs->putstr(configs, p0, p1 + 1); // NOTE - this allocs memory for it's key and it's data. + p0 = p2 + 1; + if ('\0' == *p0) + p0 = NULL; + }; + free(d); + } + +// TODO - should also load god names, and maybe the SMTP stuff. +// Note that the OpenSim SMTP stuff is ONLY for LSL script usage, we probably want to put it in the Lua config file instead. + + if (mysql_library_init(toys.optc, toys.optargs, NULL)) + { + E("mysql_library_init() failed!"); + goto finished; + } + if (!dbConnect()) + goto finished; + + // Need to kick this off. + stats = getStats(database, stats); + char *h = qstrunchar(qconfig->getstr(qconfig, "Const.HostName", false), '"', '"'); + char *p = qstrunchar(qconfig->getstr(qconfig, "Const.PublicPort", false), '"', '"'); + stats->stats->putstr(stats->stats, "grid", qstrunchar(qconfig->getstr(qconfig, "Const.GridName", false), '"', '"')); + stats->stats->putstr(stats->stats, "HostName", h); + stats->stats->putstr(stats->stats, "PublicPort", p); + snprintf(toybuf, sizeof(toybuf), "http://%s:%s/", h, p); + + stats->stats->putstr(stats->stats, "uri", toybuf); + qconfig->free(qconfig); + } + + + if (isWeb) + { + char **initialEnv = environ; + char *tmp0, *tmp1; + int count = 0, entries, bytes; + + dynPages = qhashtbl(0, 0); + newDynPage("account.html", (pageFunction) account_html); + + // FCGI_LISTENSOCK_FILENO is the socket to the web server. + // STDOUT and STDERR go to the web servers error log, or at least it does in Apache 2 mod_fcgid. + I("Running SledjChisl inside a web server, pid %d.", getpid()); + + if (0 == toys.optc) + D("no args"); + else + { + for (entries = 0, bytes = -1; entries < toys.optc; entries++) + D("ARG %s\n", toys.optargs[entries]); + } + printEnv(environ); + +/* +? https://stackoverflow.com/questions/30707792/how-to-disable-buffering-with-apache2-and-mod-proxy-fcgi + https://z-issue.com/wp/apache-2-4-the-event-mpm-php-via-mod_proxy_fcgi-and-php-fpm-with-vhosts/ + A lengthy and detailed "how to set this up with PHP" that might be useful. + https://www.apachelounge.com/viewtopic.php?t=4385 + "Also the new mod_proxy_fcgi for Apache 2.4 seems to be crippled just like mod_fcgid in terms of being limited to just one request per process at a time." + But then so is the silly fcgi2 SDK, which basically assumes it's a CGI wrapper, not proper FCGI. ++ I could even start the spawn-fcgi process from the tmux instance of sledjchisl. ++ Orrr just open the socket / port myself from the tmux instance and do the raw FCGI thing through it. +*/ + while (FCGI_Accept() != -1) + { + reqData *Rd = xzalloc(sizeof(reqData)); + + if (-1 == clock_gettime(CLOCK_REALTIME, &Rd->then)) + perror_msg("Unable to get the time."); + Rd->L = L; + Rd->configs = configs; +// Rd->queries = qhashtbl(0, 0); // Inited in toknize below. +// Rd->body = qhashtbl(0, 0); // Inited in toknize below. +// Rd->cookies = qhashtbl(0, 0); // Inited in toknize below. + Rd->headers = qhashtbl(0, 0); + Rd->valid = qhashtbl(0, 0); + Rd->stuff = qhashtbl(0, 0); + Rd->database = qhashtbl(0, 0); + Rd->Rcookies = qhashtbl(0, 0); + Rd->Rheaders = qhashtbl(0, 0); + Rd->stats = stats; + Rd->errors = qlist(0); + Rd->messages = qlist(0); + Rd->reply = qgrow(QGROW_THREADSAFE); + Rd->outQuery = xstrdup(""); + Rd->shs.status = SHS_UNKNOWN; + qhashtbl_obj_t hobj; + qlist_obj_t lobj; + + // So far I'm seeing these as all caps, but I don't think they are defined that way. Case insensitive per the spec. + // So convert them now, also "-" -> "_". +t("HEADERS"); + char **envp = environ; + for ( ; *envp != NULL; envp++) + { + char *k = xstrdup(*envp); + char *v = strchr(k, '='); + + if (NULL != v) + { + *v = '\0'; + char *ky = qstrreplace("tr", qstrupper(k), "-", "_"); + Rd->headers->putstr(Rd->headers, ky, v + 1); +if ((strcmp("HTTP_COOKIE", ky) == 0) || (strcmp("CONTENT_LENGTH", ky) == 0) || (strcmp("QUERY_STRING", ky) == 0)) + d(" %s = %s", ky, v + 1); + } + free(k); + } + + // The FCGI paramaters sent from the server, are converted to environment variablse for the fcgi2 SDK. + // The FCGI spec doesn't mention what these are. except FCGI_WEB_SERVER_ADDRS. + char *Role = Rd->headers->getstr(Rd->headers, "FCGI_ROLE", false); + Rd->Path = Rd->headers->getstr(Rd->headers, "PATH_INFO", false); +// if (NULL == Rd->Path) {msleep(1000); continue;} + char *Length = Rd->headers->getstr(Rd->headers, "CONTENT_LENGTH", false); +//char *Type = Rd->headers->getstr(Rd->headers, "CONTENT_TYPE", false); + Rd->Method = Rd->headers->getstr(Rd->headers, "REQUEST_METHOD", false); + Rd->Script = Rd->headers->getstr(Rd->headers, "SCRIPT_NAME", false); + Rd->Scheme = Rd->headers->getstr(Rd->headers, "REQUEST_SCHEME", false); + Rd->Host = Rd->headers->getstr(Rd->headers, "HTTP_HOST", false); +//char *SUri = Rd->headers->getstr(Rd->headers, "SCRIPT_URI", false); + Rd->RUri = Rd->headers->getstr(Rd->headers, "REQUEST_URI", true); +//char *Cookies = Rd->headers->getstr(Rd->headers, "HTTP_COOKIE", false); +//char *Referer = Rd->headers->getstr(Rd->headers, "HTTP_REFERER", false); +//char *RAddr = Rd->headers->getstr(Rd->headers, "REMOTE_ADDR", false); +//char *Cache = Rd->headers->getstr(Rd->headers, "HTTP_CACHE_CONTROL", false); +//char *FAddrs = Rd->headers->getstr(Rd->headers, "FCGI_WEB_SERVER_ADDRS", false); +//char *Since = Rd->headers->getstr(Rd->headers, "IF_MODIFIED_SINCE", false); + /* Per the spec CGI https://www.ietf.org/rfc/rfc3875 + meta-variable-name = "AUTH_TYPE" | "CONTENT_LENGTH" | + "CONTENT_TYPE" | "GATEWAY_INTERFACE" | + "PATH_INFO" | "PATH_TRANSLATED" | + "QUERY_STRING" | "REMOTE_ADDR" | + "REMOTE_HOST" | "REMOTE_IDENT" | + "REMOTE_USER" | "REQUEST_METHOD" | + "SCRIPT_NAME" | "SERVER_NAME" | + "SERVER_PORT" | "SERVER_PROTOCOL" | + "SERVER_SOFTWARE" + Also protocol / scheme specific ones - + HTTP_* comes from some of the request header. The rest are likely part of the other env variables. + Also covers HTTPS headers, with the HTTP_* names. + + */ + +t("COOKIES"); + Rd->cookies = toknize(Rd->headers->getstr(Rd->headers, "HTTP_COOKIE", false), "=;"); + santize(Rd->cookies); + if (strcmp("GET", Rd->Method) == 0) + { // In theory a POST has body fields INSTEAD of query fields. Especially for ignoring the linky-hashish after the validation page. +t("QUERY"); + Rd->queries = toknize(Rd->headers->getstr(Rd->headers, "QUERY_STRING", false), "=&"); + santize(Rd->queries); + } + else + { +T("ignoring QUERY"); + Rd->queries = qhashtbl(0, 0); + free(Rd->RUri); + Rd->RUri = xmprintf("%s%s", Rd->Script, Rd->Path); + } +t("BODY"); + char *Body = NULL; + if (Length != NULL) + { + size_t len = strtol(Length, NULL, 10); + Body = xmalloc(len + 1); + int c = FCGI_fread(Body, 1, len, FCGI_stdin); + if (c != len) + { + E("Tried to read %d of the body, only got %d!", len, c); + } + Body[len] = '\0'; + } + else + Length = "0"; + Rd->body = toknize(Body, "=&"); + free(Body); + santize(Rd->body); + + I("%s %s://%s%s -> %s%s", Rd->Method, Rd->Scheme, Rd->Host, Rd->RUri, webRoot, Rd->Path); + D("Started FCGI web request ROLE = %s, body is %s bytes, pid %d.", Role, Length, getpid()); + + if (NULL == Rd->Path) + { + E("NULL path in FCGI request!"); + Rd->Rheaders->putstr(Rd->Rheaders, "Status", "404 Not Found"); + goto sendReply; + } + +/* TODO - other headers may include - + different Content-type + Status: 304 Not Modified + Last-Modified: timedatemumble + https://en.wikipedia.org/wiki/List_of_HTTP_header_fields +*/ + Rd->Rheaders->putstr(Rd->Rheaders, "Status", "200 OK"); + Rd->Rheaders->putstr(Rd->Rheaders, "Content-type", "text/html"); +// TODO - check these. + // This is all dynamic web pages, and likeley secure to. + // Most of these are from https://www.smashingmagazine.com/2017/04/secure-web-app-http-headers/ + // https://www.twilio.com/blog/a-http-headers-for-the-responsible-developer is good to, and includes useful compression and image stuff. + // On the other hand, .css files are referenced, which might be better off being cached, so should tweak some of thees. + Rd->Rheaders->putstr(Rd->Rheaders, "Cache-Control", "no-cache, no-store, must-revalidate"); + Rd->Rheaders->putstr(Rd->Rheaders, "Pragma", "no-cache"); + Rd->Rheaders->putstr(Rd->Rheaders, "Expires", "-1"); + Rd->Rheaders->putstr(Rd->Rheaders, "Strict-Transport-Security", "max-age=63072000"); // Two years. +// TODO - do something about this - + /* https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/style-src + "Note: Disallowing inline styles and inline scripts is one of + the biggest security wins CSP provides. However, if you + absolutely have to use it, there are a few mechanisms that + will allow them." + + WTF? And the mechanisms include nonces, hashes, or 'unsafe-inline'. + Not sure why inline styles need to be that secure, when downloaded ones are not. + Ah, it's for user input that is sent back to other users, they might include funky CSS in their input. + SOOOO, proper validation and escaping is needed. + OOOOR, use the nonce, and make it a different nonce per page serve. + OOOOR, just put all the style stuff in a .css file. Then we can use style-src 'self' without the 'unsafe-inline'? + There's only one block of