From 3f67f0304f2138a38cf2695afec163b12db02d2c Mon Sep 17 00:00:00 2001 From: onefang Date: Mon, 16 Mar 2020 20:22:12 +1000 Subject: Finally add the sledjchisl C & Lua maanger, and friends. --- RunIt.sh | 4 + bin/.sledjChisl.conf.lua | 1 + bin/sledjchisl | 1 + example/etc/apache2/sledjchisl.fcgi.conf | 37 + src/.sledjChisl.conf.lua | 26 + src/BuildIt.sh | 140 +- src/miniconfig | 1 + src/sledjchisl/NOTES.txt | 503 +++ src/sledjchisl/README | 51 + src/sledjchisl/fcgi_SC.c | 13 + src/sledjchisl/fcgi_SC.h | 136 + src/sledjchisl/script.lua | 18 + src/sledjchisl/sledjchisl.c | 5080 ++++++++++++++++++++++++++++++ 13 files changed, 5991 insertions(+), 20 deletions(-) create mode 100755 RunIt.sh create mode 120000 bin/.sledjChisl.conf.lua create mode 120000 bin/sledjchisl create mode 100644 example/etc/apache2/sledjchisl.fcgi.conf create mode 100644 src/.sledjChisl.conf.lua create mode 100644 src/sledjchisl/NOTES.txt 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 diff --git a/RunIt.sh b/RunIt.sh new file mode 100755 index 0000000..55ed7c3 --- /dev/null +++ b/RunIt.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +cd bin +./sledjchisl diff --git a/bin/.sledjChisl.conf.lua b/bin/.sledjChisl.conf.lua new file mode 120000 index 0000000..21cf12d --- /dev/null +++ b/bin/.sledjChisl.conf.lua @@ -0,0 +1 @@ +../src/.sledjChisl.conf.lua \ No newline at end of file diff --git a/bin/sledjchisl b/bin/sledjchisl new file mode 120000 index 0000000..1f2033b --- /dev/null +++ b/bin/sledjchisl @@ -0,0 +1 @@ +../src/build/toybox/generated/unstripped/toybox \ No newline at end of file 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 + + + ServerName localhost + ServerAdmin webmaster@localhost + + DocumentRoot /var/www/html + + AllowOverride All + + + + SetHandler "proxy:unix:///opt/opensim_SC/caches/sledjchisl.socket|fcgi://localhost/" + + + ErrorLog ${APACHE_LOG_DIR}/error.log + CustomLog ${APACHE_LOG_DIR}/access.log combined + + + + + ServerName local + ServerAlias *.localhost + ServerAdmin webmaster@localhost + + DocumentRoot /var/www/html + + AllowOverride All + + + + SetHandler "proxy:unix:///opt/opensim_SC/caches/sledjchisl.socket|fcgi://localhost/" + + + diff --git a/src/.sledjChisl.conf.lua b/src/.sledjChisl.conf.lua new file mode 100644 index 0000000..849a203 --- /dev/null +++ b/src/.sledjChisl.conf.lua @@ -0,0 +1,26 @@ +-- 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 = +{ + ["scRoot"] = "/opt/opensim_SC"; + ["scUser"] = "opensimsc"; + ["Tconsole"] = "SledjChisl"; + ["Tsocket"] = "caches/opensim-tmux.socket"; + ["Ttab"] = "SC"; + ["loadAverageInc"] = 0.7; + ["simTimeOut"] = 45; -- seconds + ["webRoot"] = "/var/www"; + ["URL"] = "sledjchisl.fcgi"; + ["seshTimeOut"] = 30 * 60; -- seconds + ["idleTimeOut"] = 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."; +} +return config diff --git a/src/BuildIt.sh b/src/BuildIt.sh index d199e95..2feb6f4 100755 --- a/src/BuildIt.sh +++ b/src/BuildIt.sh @@ -1,33 +1,133 @@ #!/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 "" +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 "" +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 "" +echo "" + if [ ! -d git-sub-modules/toybox ]; then - pushd git-sub-modules + pushd git-sub-modules >/dev/null git clone https://github.com/landley/toybox.git - popd + popd >/dev/null else - pushd git-sub-modules/toybox - git pull - popd + 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 -pushd git-sub-modules/toybox -#git stash save -#git pull -#git stash pop -popd +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 -mkdir -p build -rm -fr build/toybox -cp -r git-sub-modules/toybox build/ -ln -fs ../../../boxes build/toybox/toys/boxes -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 "" +echo "" pushd build/toybox >/dev/null -make clean -#make defconfig -##make menuconfig -make allnoconfig KCONFIG_ALLCONFIG=../../miniconfig || exit 1 +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 + + +sudo rm -f /opt/opensim_SC/caches/sledjchisl.socket +sudo spawn-fcgi -n -u opensimsc -s /opt/opensim_SC/caches/sledjchisl.socket -M 0660 -G www-data -- /usr/bin/valgrind build/toybox/generated/unstripped/toybox sledjchisl +#sudo spawn-fcgi -n -u opensimsc -s /opt/opensim_SC/caches/sledjchisl.socket -M 0660 -G www-data -- /usr/bin/ddd build/toybox/generated/unstripped/toybox sledjchisl diff --git a/src/miniconfig b/src/miniconfig index a8b01c4..9f8fe88 100644 --- a/src/miniconfig +++ b/src/miniconfig @@ -5,6 +5,7 @@ CONFIG_RM=y CONFIG_SH=y CONFIG_SU=y CONFIG_UUIDGEN=y +CONFIG_SLEDJCHISL=y CONFIG_TOYBOX_FREE=y CONFIG_TOYBOX_HELP=y CONFIG_TOYBOX_HELP_DASHDASH=y diff --git a/src/sledjchisl/NOTES.txt b/src/sledjchisl/NOTES.txt new file mode 100644 index 0000000..a5f0861 --- /dev/null +++ b/src/sledjchisl/NOTES.txt @@ -0,0 +1,503 @@ +I'm re-purposing this for SledjHamr https://sledjhamr.org/git/docs/index.html + +The general structure of SledjHamr is a bunch of servers talking to each +other via Internet (or just local) connections. One of them is a web +server for assets, world data, and inventory. + +Originally I didn't think using a web based world client was a good idea, +however it might be better to have one, for reasons. Now I need a web +management console that can do all the things the current tmux console +can, including OpenSim console and commands. Plus account management for +users. I can also use a web based Jabber / XMPP front end to chat, IM, +and group chatter, which would run in the normal viewers web browser. +This provides a doorway into putting SledjHamr stuff in existing viewers +without needing them to support it. So a web based viewer now makes more +sense, and also means we can get away with not needing a viewer at all. + +Toybox itself doesn't include a web server, and I don't think there is +one on the roadmap. So we have to use an external web server, which was +a design goal of SledjHamr in the first place, using existing mature +HTTP infrastructure, coz that's already solved problems for a bunch of +things that plague OS/SL to this day. Clear your cache! Pffft. + +So sledjchisl.c will be the "love world server", though initially it just +drives OpenSim_SC in tmux via tmux commands to send keys and read output. +Later it might run opensim_SC directly and use STDIN and STDOUT to do +everything. It'll also provide the text management front end that runs +in the left tmux panel of the first window, which is why it's based on +boxes in the first place. Later still it can take over opensim_SC +functions as I move them out of mono. + +We will need a text, web, and GUI version of this management front end. +Hmmm, maybe don't need a GUI version, GUI users can just run a terminal. + +After much research, FastCGI / FCGI seems to be the most portable way of +interfacing with existing web servers. FCGI protocol closes STDERR and +STDOUT, and uses STDIN as it's two way communications channel to the web +server, so our FCGI module can't be used as the text management front +end. This is probably a good idea to keep them seperate anyway, for +security, coz the web server is exposed to the world, the console isn't. + +Currently sledjchisl.c tests to see if it's running in tmux already, if +it isn't it starts up tmux runs itself into this new tmux, then exits. +So it could also test if it's running from FCGI, and switch to web mode, +then it'll need to find the tmuxed instance to send commands to it. +Either via nails connection, or sending tmux commands via shell. + +FCGI has methods of dealing with auth and templates. B-) + +So for now I think I'll have the text and web management front ends in +sledjchisl.c, and the love world server as well. I can split them up +later if I need to. + + + + +I has Apache 2.4.25-3+deb9u9 + MariaDB 10.1.44-MariaDB + + +https://gist.github.com/dermesser/e2f9b66457ae19ebd116 + Multithreaded example in C. + + +------------------------------------------------------------------- + +Apache doesn't seem to support FCGI filter role, so I might have to do +without. Might be better anyway. + + +"A Filter is similar in functionality to a Responder that takes a data +file as a parameter. The difference is that with a Filter, both the data +file and the Filter itself can be access controlled using the Web +server's access control mechanisms, while a Responder that takes the name +of a data file as a parameter must perform its own access control checks +on the data file." + + Which is fine, our access control checks will be "Is this database + defined user already logged on via our FCGI script?". We should have + total control over that. I was planning on using the FCGI auth + mechanism anyway. + + +RESPONDER + web server sends FCGI_PARAMS + CONTENT_LENGTH + web server sends input body FCGI_STDIN + fcgi app sends result data over FCGI_STDOUT and error messages over FCGI_STDERR + it has to finish reading FCGI_PARAMS first + fcgi app sends FCGI_END_REQUEST(protocolStatus = FCGI_REQUEST_COMPLETE) + + +FILTER + filtered file has last modified time + web server sets FCGI_DATA_LAST_MOD accordingly + web server sends FCGI_PARAMS + CONTENT_LENGTH FCGI_DATA_LAST_MOD FCGI_DATA_LENGTH + web server sends input body FCGI_STDIN + web servers sends file over FCGI_DATA + fcgi app can ignore FCGI_DATA and use it's own cached copy based on FCGI_DATA_LAST_MOD + fcgi app sends result data over FCGI_STDOUT and error messages over FCGI_STDERR + it has to finish reading FCGI_STDIN first, but not FCGI_DATA + fcgi app sends FCGI_END_REQUEST(protocolStatus = FCGI_REQUEST_COMPLETE) + + +Soooo, FILTER might be slower anyway if we are caching the filtered file, +or mmapping it, coz filter has to start sending the filtered file, even +if it's to be served from cache. Plus no need to wait for FCGI_STDIN +before spewing it out. + + +Last update time for parameters, plus an update frequency. Once a minute. + + NOTE - SSI is a bit more complex than what I'm currently using. + https://en.wikipedia.org/wiki/Server_Side_Includes + + + + +. + + + + + + + + + + + + + + + https://www.w3.org/Jigsaw/Doc/User/SSI.html + Adds lots of others, including Java stuff. + Mine + + + + +------------------------------------------------------------------- + +Account creation process in the database. + +Apart from the usual input validation of things... + + +OpenSim/Server/Handlers/UserAccounts/UserAccountServerPostHandler.cs + byte[] CreateUser(Dictionary request) + Looks like their built in web front end, perhaps what is triggered by the console? + createdUserAccount + = ((UserAccountService)m_UserAccountService).CreateUser(scopeID, principalID, firstName, lastName, password, email, model); + +OpenSim/opensim-SC/OpenSim/ApplicationPlugins/RemoteController/RemoteAdminPlugin.cs + An XML RPC interface to - + private UserAccount CreateUser(UUID scopeID, string firstName, string lastName, string password, string email) + account = new UserAccount(scopeID, UUID.Random(), firstName, lastName, email); + if (userAccountService.StoreUserAccount(account)) + success = authenticationService.SetPassword(account.PrincipalID, password) + gridUserService.SetHome(account.PrincipalID.ToString(), home.RegionID, new Vector3(128, 128, 0), new Vector3(0, 1, 0)); + success = inventoryService.CreateUserInventory(account.PrincipalID); + +OpenSim/opensim-SC/OpenSim/Services/UserAccountService/UserAccountService.cs + Looks like the console command handler. + create user [ [ [ [ [ []]]]]] - Create a new user + protected void HandleCreateUser(string module, string[] cmdparams) + Gathers console arguments, or prompts for them. + CreateUser(UUID.Zero, principalId, firstName, lastName, password, email, model); + public UserAccount CreateUser(UUID scopeID, UUID principalID, string firstName, string lastName, string password, string email, string model = "") + Looks almost identical to the OpenSim/ApplicationPlugins/RemoteController/RemoteAdminPlugin.cs one above, but they add - + CreateDefaultAppearanceEntries(account.PrincipalID) + + + +account = new UserAccount(scopeID, UUID.Random(), firstName, lastName, email); + OpenSim/opensim-SC/OpenSim/Services/Interfaces/IUserAccountService.cs + public UserAccount(UUID scopeID, UUID principalID, string firstName, string lastName, string email) + Just holds the data in memory, in a dictionary I think. + OpenSim/opensim-SC/OpenSim/Services/UserAccountService/UserAccountService.cs + public bool StoreUserAccount(UserAccount data) + Stuffs the data into a new UserAccountData() + m_Database.Store(d) + As far as I can tell, just dumps this data into the UserAccounts table - + FirstName, LastName, PrincipleID, ScopeID, Email, Created, UserLevel, UserFlags, UserTitle + PrincipleID is their randomly generated with no thought to collisions UUID. + ScopeID is 00000000-0000-0000-0000-000000000000 + Userlevel is 0 for most, -1 for Waki, determines if they can log on. Also higher for gods and things. + UserFlags, I think the only one is "64 god can login to this account using gods password. + UserTitle might default to "Local", or be configurable / and editable. + something something URL encoded "ServiceURLs" mumble + HomeURI=http%3a%2f%2fgrid.infinitegrid.org%3a8002%2f GatekeeperURI= InventoryServerURI=http%3a%2f%2fgrid.infinitegrid.org%3a8002%2f AssetServerURI=http%3a%2f%2fgrid.infinitegrid.org%3a8002%2f ProfileServerURI=http%3a%2f%2fgrid.infinitegrid.org%3a8002%2f FriendsServerURI=http%3a%2f%2fgrid.infinitegrid.org%3a8002%2f IMServerURI=http%3a%2f%2fgrid.infinitegrid.org%3a8002%2f GroupsServerURI=http%3a%2f%2fgrid.infinitegrid.org%3a8002%2f + Though most are either NULL, empty, or - + HomeURI= GatekeeperURI= InventoryServerURI= AssetServerURI= + Doesn't metion "active", which is always equal to 1 I guess. + + + +success = authenticationService.SetPassword(account.PrincipalID, password) + OpenSim/Services/AuthenticationService/AuthenticationServiceBase.cs + stores password details in "auth" table - + UUID + passwordSalt = Util.Md5Hash(UUID.Random().ToString()); + passwdHash = Util.Md5Hash(Util.Md5Hash(password) + ":" + passwordSalt); + accountType = "UserAccount"; + webLoginKey = UUID.Zero.ToString(); + + + +gridUserService.SetHome(account.PrincipalID.ToString(), home.RegionID, new Vector3(128, 128, 0), new Vector3(0, 1, 0)); + OpenSim/Services/UserAccountService/GridUserService.cs + Stores in database table GridUser + HomeRegionID, HomePosition, HomeLookAt + The other fields in that table - + UserID, LastRegionID, LastPosition, LastLookAt, Online (true or false), Login (timestamp or 0), Logout (timestamp or 0). + + + +success = inventoryService.CreateUserInventory(account.PrincipalID); + OpenSim/Services/InventoryService/XInventoryService.cs + Create a bunch of folders in the users inventory, of specific types. + rootFolder = ConvertToOpenSim(CreateFolder(principalID, UUID.Zero, (int)FolderType.Root, InventoryFolderBase.ROOT_FOLDER_NAME)); + XInventoryFolder[] sysFolders = GetSystemFolders(principalID, rootFolder.ID) + if (!Array.Exists(sysFolders, delegate(XInventoryFolder f) { if (f.type == (int)FolderType.Animation) return true; return false; })) + CreateFolder(principalID, rootFolder.ID, (int)FolderType.Animation, "Animations"); + FolderType.BodyPart, "Body Parts" + XInventoryFolder folder = CreateFolder(principalID, rootFolder.ID, (int)FolderType.CallingCard, "Calling Cards"); + folder = CreateFolder(principalID, folder.folderID, (int)FolderType.CallingCard, "Friends") + CreateFolder(principalID, folder.folderID, (int)FolderType.CallingCard, "All"); + FolderType.Clothing, "Clothing" + FolderType.CurrentOutfit, "Current Outfit" + FolderType.Favorites, "Favorites" + FolderType.Gesture, "Gestures") + FolderType.Landmark, "Landmarks" + FolderType.LostAndFound, "Lost And Found" + FolderType.Notecard, "Notecards" + FolderType.Object, "Objects" + FolderType.Snapshot, "Photo Album" + FolderType.LSLText, "Scripts" + FolderType.Sound, "Sounds" + FolderType.Texture, "Textures" + FolderType.Trash, "Trash" + + Stores in database inventoryFolders ???? + folderName, type, version = 1, folderID = UUID.Random(), agentID = principalID, parentFolderID = parentID + + + + +CreateDefaultAppearanceEntries(account.PrincipalID) + OpenSim/Services/UserAccountService/UserAccountService.cs + protected void CreateDefaultAppearanceEntries(UUID principalID) + Creates a bunch of "Default *" body parts and clothes, Ruth 1.0, links them in Inventories current outfit folder. + Creates a AvatarWearable[] and puts them all in it. + AvatarAppearance ap = new AvatarAppearance(); + ap.SetWearable(i, wearables[i]); + m_AvatarService.SetAppearance(principalID, ap); + + + + + +UserAccounts table - + UserFlags 64 is "allow gods to log in as me" + 0xf00 is membershipType, unles there's a title. Only sent to viewers I think. + 32 is Minors for estate banning purposes. + 4 is Anonymous for estate banning purposes. + 1 is AllowPublish in profile, but userprofile has this as separate field. + 2 is MaturePublish in profile, but userprofile has this as separate field. +Presence table - + UserID varchar(255) + RegionID char(36) + SessionID char(36) + SecureSessionID char(36) + LastSeen timestamp +tokens table (I think this is actually used for something) - + UUID char(36) + token varchar(255) current example looks like a UUID. + validity datetime +userdata (empty, can't find any actual usage in the source code, part of profiles) - + UserId char(36) primary index + TagId varchar(64) primary index + DataKey varchar(255) + DataVal varchar(255) +auth.webLoginKey seems to be some sort of passwordy type thing, though perhaps not actually hashed, rarely used, none of IG members have one. + + +PLAN- +. username +. password +. create login + +.check if it's a proper two word name +.login -> check if it's an existing account, get their UUID. + create toke_n_munchie + write session record + +create -> new user + create new UUID + check if it's an existing UUID + dbCount(, "UserAccounts", "PrincipleID='new-UUID'") + loop until we get a new one + create toke_n_munchie + write session record + + + Create -> + (wait a few seconds before showing this page) +. email +. email again +. password again +. DoB +. accept terms of service +. claim to be an adult +. confirm / cancel + + New user + UserAccounts.FirstName = ??? + UserAccounts.LastName = ??? + UserAccounts.Email = ??? + UserAccounts.Created = timestamp + UserAccounts.PrincipleID = randomly generate UUID, but check for collisions with other accounts. + It's a UNIQUE KEY. + UserAccounts.ScopeID = 00000000-0000-0000-0000-000000000000 + UserAccounts.Userlevel = -200 + UserAccounts.UserFlags = 64 + UserAccounts.UserTitle = newbie + UserAccounts.ServiceURLs = "" + UserAccounts.active = 0 + + auth.UUID = UserAccounts.PrincipleID + It's a PRIMARY KEY. + auth.passwordSalt = Util.Md5Hash(UUID.Random().ToString()) + auth.passwdHash = Util.Md5Hash(Util.Md5Hash(password) + ":" + passwordSalt) + auth.accountType = "UserAccount" + auth.webLoginKey (varchar(255)) = "00000000-0000-0000-0000-000000000000" + + userdata.UserId = UserAccounts.PrincipleID + userdata.TagId = "account creation data" + It's a UNIQUE KEY + userdata.DataKey = "DoB" + userdata.DataVal = ??? + + userdata.UserId = UserAccounts.PrincipleID + userdata.TagId = "account creation data" + userdata.DataKey = "timezone" + userdata.DataVal = ??? + + userdata.UserId = UserAccounts.PrincipleID + userdata.TagId = "account creation data" + userdata.DataKey = "Terms of service" + userdata.DataVal = "True" + + userdata.UserId = UserAccounts.PrincipleID + userdata.TagId = "account creation data" + userdata.DataKey = "claims to be an adult" + userdata.DataVal = "True" + + + Validated via email + (wait a few seconds before showing this page) + UserAccounts.Userlevel = -100 + UserAccounts.UserTitle = validated + + + Vouched for + userdata.UserId = UserAccounts.PrincipleID + userdata.TagId = "vouches" + userdata.DataKey = UUID of voucher + userdata.DataVal = timestamp of vouching + + UserAccounts.Userlevel = -50 + UserAccounts.UserTitle = vouched for + + + Admin approved + GridUser.UserID = UserAccounts.PrincipleID + It's a PRIMARY KEY. + GridUser.HomeRegionID = ??? + GridUser.HomePosition = ??? + GridUser.HomeLookAt = ??? + GridUser.LastRegionID = ??? + GridUser.LastPosition = ??? + GridUser.LastLookAt = ??? + GridUser.Online = False + GridUser.Login = 0 + GridUser.Logout = 0 + + UserAccounts.active = 1 + UserAccounts.Userlevel = 1 + UserAccounts.UserTitle = Member / Local / whatever + + Load the default IAR. + + +------------------------------------------------------------------- + +https://project-awesome.org/aleksandar-todorovic/awesome-c + A curated list of C good stuff. + +https://wolkykim.github.io/qdecoder/ + CGI library made by the qlibc guy, does support FCGI. + Might be a wrapper around the fcgi_stdio stuff I'm already using? + + +https://danielmiessler.com/study/http/ + A Security-focused HTTP Primer + Nothing much new except to say this about the Referer header - + "should not be used to make security decisions as it is controlled by the client" + Though others tell us to do precisely that. lol + + +------------------------------------------------------------------- + +apt install libmariadbclient-dev libapache2-mod-fcgid + +------------------------------------------------------------------- + +Merge it into OpenSim-SC. + + Complication - I had already added the boxes + early sledjchisl.c to opensim-SC. + I may have to revert that lot, only a few minor commits, which are already part of the main boxes. + d9c772712e27c8e25fab0d17555a9bc11017a125 + d4ea3e50173df1ad646bdb7dc802f5d320b7e511 + 10ed36b3452ce6373175112716b043047dc896a9 + 2f66c46e7ce18d60cd1f565880fb7762b3399ccb + 34c5ee4c2a489a506e93d5b303fbc80b263747f0 is the commit that added it. + f9bfa831d1ccaa973c42042584510e1a724ddaef the one before it. + It's been pushed up to the sledjhamr.org repo. + + git revert d9c772712e27c8e25fab0d17555a9bc11017a125 d4ea3e50173df1ad646bdb7dc802f5d320b7e511 10ed36b3452ce6373175112716b043047dc896a9 2f66c46e7ce18d60cd1f565880fb7762b3399ccb 34c5ee4c2a489a506e93d5b303fbc80b263747f0 + git remote add -f boxes ~/MyLinux_3/TOYBOX/sledjchisl + git merge --allow-unrelated-histories boxes/master + git remote rm boxes + + +https://medium.com/altcampus/how-to-merge-two-or-multiple-git-repositories-into-one-9f8a5209913f +https://medium.com/@checko/merging-two-git-repositories-into-one-preserving-the-git-history-4e20d3fafa4e +https://thoughts.t37.net/merging-2-different-git-repositories-without-losing-your-history-de7a06bba804 +https://blog.doismellburning.co.uk/merging-two-git-repositories/ + +Other much more complicated variations. + +http://mbork.pl/2019-08-19_Transplanting_a_directory_to_another_Git_repository + + +------------------------------------------------------------------- + +Install / update / upgrade. + +I could keep the version number around. + Include version numbers / branches of dependencies. + Update will grab any security updates for the installed version. + Upgrade will upgrade to a chosen later different version. + Downgrade will downgrade to a chosen earlier different version. + +Note that we are currently using the LuaJIT 2.1.0-beta3 branch of the +main Luajit repo. Everything else is on their master branches. + +Bootstrap - + bootstrap.sh or bootstrap.bat + + Build the LuaJIT that comes with our source. It "builds out-of-the + box on most systems" and has no dependencies, other than a C build system. + + Or download a prebuilt LuaJIT from somewhere. + + After toybox has been LuaJITized. + + Build the LuaJIT that comes with our source. It "builds out-of-the + box on most systems" and has no dependencies, other than a C build system. + + Similar should apply to toybox, though it's our LuaJITized version. + Will need a specific miniconfig for this that doesn't include sledjchisl. + + Or download a prebuilt toybox+LuaJIT from a SledjHamr package repo. + +Install - + install.lua + + Will need a pre flight check if the dependencies are installed. + It checks if the system is built and has source. + Build it all. + Do the usual copy stuff to a directory thing. + Run "sledjchisl -install" in that directory. + Which does the usual "check health of system and fix up problems" thing, then quits instead of keep running. + The health check should include making sure our database creds exist / work. + +Update / upgrade / downgrade + install.lua -update + install.lua -upgrade + install.lua -downgrade + + Check if we are a binary only, or a source install. + wget new binaries / git pull new source + Toybox has a wget in pending, otherwise it only has ftpget. + Git is standalone outside of the system, but if you are + running from source, you likely have standard build tools + like git. + + +Yeah I hate things that have their own packaging system, for needing to +step outside the operating systems packaging system, and adding to the too +long list of stuff I have to deal with manually, and now I are one. lol diff --git a/src/sledjchisl/README b/src/sledjchisl/README new file mode 100644 index 0000000..5d0a7b1 --- /dev/null +++ b/src/sledjchisl/README @@ -0,0 +1,51 @@ +I'm re-purposing this for SledjHamr https://sledjhamr.org/git/docs/index.html + +The general structure of SledjHamr is a bunch of servers talking to each +other via Internet (or just local) connections. One of them is a web +server for assets, world data, and inventory. + +Originally I didn't think using a web based world client was a good idea, +however it might be better to have one, for reasons. Now I need a web +management console that can do all the things the current tmux console +can, including OpenSim console and commands. Plus account management for +users. I can also use a web based Jabber / XMPP front end to chat, IM, +and group chatter, which would run in the normal viewers web browser. +This provides a doorway into putting SledjHamr stuff in existing viewers +without needing them to support it. So a web based viewer now makes more +sense, and also means we can get away with not needing a viewer at all. + +Toybox itself doesn't include a web server, and I don't think there is +one on the roadmap. So we have to use an external web server, which was +a design goal of SledjHamr in the first place, using existing mature +HTTP infrastructure, coz that's already solved problems for a bunch of +things that plague OS/SL to this day. Clear your cache! Pffft. + +So sledjchisl.c will be the "love world server", though initially it just +drives OpenSim_SC in tmux via tmux commands to send keys and read output. +Later it might run opensim_SC directly and use STDIN and STDOUT to do +everything. It'll also provide the text management front end that runs +in the left tmux panel of the first window, which is why it's based on +boxes in the first place. Later still it can take over opensim_SC +functions as I move them out of mono. + +We will need a text, web, and GUI version of this management front end. +Hmmm, maybe don't need a GUI version, GUI users can just run a terminal. + +After much research, FastCGI / FCGI seems to be the most portable way of +interfacing with existing web servers. FCGI protocol closes STDERR and +STDOUT, and uses STDIN as it's two way communications channel to the web +server, so our FCGI module can't be used as the text management front +end. This is probably a good idea to keep them seperate anyway, for +security, coz the web server is exposed to the world, the console isn't. + +Currently sledjchisl.c tests to see if it's running in tmux already, if +it isn't it starts up tmux runs itself into this new tmux, then exits. +So it could also test if it's running from FCGI, and switch to web mode, +then it'll need to find the tmuxed instance to send commands to it. +Either via nails connection, or sending tmux commands via shell. + +FCGI has methods of dealing with auth and templates. B-) + +So for now I think I'll have the text and web management front ends in +sledjchisl.c, and the love world server as well. I can split them up +later if I need to. diff --git a/src/sledjchisl/fcgi_SC.c b/src/sledjchisl/fcgi_SC.c new file mode 100644 index 0000000..36aba77 --- /dev/null +++ b/src/sledjchisl/fcgi_SC.c @@ -0,0 +1,13 @@ +/* fcgi_SC.h - Generic fcgi handler, coz the others all suck. + * + * Copyright 2020 David Seikel + */ + +// I use camelCaseNames internally, instead of underscore_names as is preferred +// in the rest of toybox. A small limit of 80 characters per source line infers +// shorter names should be used. CamelCaseNames are shorter. Externally visible +// stuff is underscore_names as usual. Plus, I'm used to camelCaseNames, my +// fingers twitch that way. + +#include "toys.h" +#include "fcgi_SC.h" diff --git a/src/sledjchisl/fcgi_SC.h b/src/sledjchisl/fcgi_SC.h new file mode 100644 index 0000000..2b3fa65 --- /dev/null +++ b/src/sledjchisl/fcgi_SC.h @@ -0,0 +1,136 @@ +/* fcgi_SC.h - Generic fcgi handler, coz the oters all suck. + * + * Copyright 2020 David Seikel + */ + +enum fcgiEventType{ + FSC_CSI, + FSC_KEYS, + FSC_MOUSE, + FSC_RAW +}; + +struct fcgiEvent { + enum fcgiEventType type; // The type of this event. + char *sequence; // Either a translated sequence, or raw bytes. + int isTranslated; // Whether or not sequence is translated. + int count; // Number of entries in params. + int *params; // For CSI events, the decoded parameters. +}; + + + + +// From the spec. + +/* + * Listening socket file number + */ +#define FCGI_LISTENSOCK_FILENO 0 + +typedef struct { + unsigned char version; + unsigned char type; + unsigned char requestIdB1; + unsigned char requestIdB0; + unsigned char contentLengthB1; + unsigned char contentLengthB0; + unsigned char paddingLength; + unsigned char reserved; +} FCGI_Header; + +/* + * Number of bytes in a FCGI_Header. Future versions of the protocol + * will not reduce this number. + */ +#define FCGI_HEADER_LEN 8 + +/* + * Value for version component of FCGI_Header + */ +#define FCGI_VERSION_1 1 + +/* + * Values for type component of FCGI_Header + */ +#define FCGI_BEGIN_REQUEST 1 +#define FCGI_ABORT_REQUEST 2 +#define FCGI_END_REQUEST 3 +#define FCGI_PARAMS 4 +#define FCGI_STDIN 5 +#define FCGI_STDOUT 6 +#define FCGI_STDERR 7 +#define FCGI_DATA 8 +#define FCGI_GET_VALUES 9 +#define FCGI_GET_VALUES_RESULT 10 +#define FCGI_UNKNOWN_TYPE 11 +#define FCGI_MAXTYPE (FCGI_UNKNOWN_TYPE) + +/* + * Value for requestId component of FCGI_Header + */ +#define FCGI_NULL_REQUEST_ID 0 + +typedef struct { + unsigned char roleB1; + unsigned char roleB0; + unsigned char flags; + unsigned char reserved[5]; +} FCGI_BeginRequestBody; + +typedef struct { + FCGI_Header header; + FCGI_BeginRequestBody body; +} FCGI_BeginRequestRecord; + +/* + * Mask for flags component of FCGI_BeginRequestBody + */ +#define FCGI_KEEP_CONN 1 + +/* + * Values for role component of FCGI_BeginRequestBody + */ +#define FCGI_RESPONDER 1 +#define FCGI_AUTHORIZER 2 +#define FCGI_FILTER 3 + +typedef struct { + unsigned char appStatusB3; + unsigned char appStatusB2; + unsigned char appStatusB1; + unsigned char appStatusB0; + unsigned char protocolStatus; + unsigned char reserved[3]; +} FCGI_EndRequestBody; + +typedef struct { + FCGI_Header header; + FCGI_EndRequestBody body; +} FCGI_EndRequestRecord; + +/* + * Values for protocolStatus component of FCGI_EndRequestBody + */ +#define FCGI_REQUEST_COMPLETE 0 +#define FCGI_CANT_MPX_CONN 1 +#define FCGI_OVERLOADED 2 +#define FCGI_UNKNOWN_ROLE 3 + +/* + * Variable names for FCGI_GET_VALUES / FCGI_GET_VALUES_RESULT records + */ +#define FCGI_MAX_CONNS "FCGI_MAX_CONNS" +#define FCGI_MAX_REQS "FCGI_MAX_REQS" +#define FCGI_MPXS_CONNS "FCGI_MPXS_CONNS" + +typedef struct { + unsigned char type; + unsigned char reserved[7]; +} FCGI_UnknownTypeBody; + +typedef struct { + FCGI_Header header; + FCGI_UnknownTypeBody body; +} FCGI_UnknownTypeRecord; + diff --git a/src/sledjchisl/script.lua b/src/sledjchisl/script.lua new file mode 100644 index 0000000..1e4b909 --- /dev/null +++ b/src/sledjchisl/script.lua @@ -0,0 +1,18 @@ +-- script.lua + +-- This works coz LuaJIT automatically loads the jit module. +if type(jit) == 'table' then + io.write('script.lua is being run by ' .. jit.version .. ' under ' .. jit.os .. ' on a ' .. jit.arch .. '\n') +else + io.write('script.lua is being run by Lua version ' .. _VERSION .. '\n') +end + +-- Receives a table, returns the sum of its components. +io.write("The table the script received has:\n"); +x = 0 +for i = 1, #foo do + print(i, foo[i]) + x = x + foo[i] +end +io.write("Returning data back to C\n"); +return x diff --git a/src/sledjchisl/sledjchisl.c b/src/sledjchisl/sledjchisl.c new file mode 100644 index 0000000..b5dfc97 --- /dev/null +++ b/src/sledjchisl/sledjchisl.c @@ -0,0 +1,5080 @@ +/* sledjchisl.c - opensim-SC management system. + * + * Copyright 2020 David Seikel + * Not in SUSv4. An entirely new invention, thus no web site either. + +USE_SLEDJCHISL(NEWTOY(sledjchisl, "m(mode):", TOYFLAG_USR|TOYFLAG_BIN)) + +config SLEDJCHISL + bool "sledjchisl" + default y + help + usage: sledjchisl [-m|--mode mode] + + opensim-SC management system. +*/ + + +// TODO - figure out how to automate testing of this. +// Being all interactive and involving external web servers / viewers makes it hard. + +// TODO - do all the setup on first run, and check if needed on each start up, to be self healing. + +// TODO - create any missing directories when we need them. + +// TODO - use a FHS compatible setup - +// scRoot = / /usr/local /opt/opensim_SC +// /etc/opensim_SC /usr/local/etc/opensim_SC /opt/opensim_SC/etc +// /run/opensim_SC /run/opensim_SC /run/opensim_SC +// /usr/bin /usr/local/bin /opt/opensim_SC/bin +// The problem here is that OpenSim already uses opensim/bin for all of it's crap, where they mix it all together. +// Don't want it in the path. +// Should put the .dlls into ../lib.. +// Most of the rest is config files or common assets. +// /usr/lib/opensim_SC /usr/local/lib/opensim_SC /opt/opensim_SC/lib +// /var/backups/opensim_SC /var/local/backups/opensim_SC /opt/opensim_SC/var/backups +// /var/cache/opensim_SC /var/local/cache/opensim_SC /opt/opensim_SC/var/cache +// /var/lib/opensim_SC /var/local/lib/opensim_SC /opt/opensim_SC/var/lib +// /var/log/opensim_SC /var/local/log/opensim_SC /opt/opensim_SC/var/log +// /var/www/opensim_SC /var/local/www/opensim_SC /opt/opensim_SC/var/www + +// TODO - pepper could be entered on the console on startup if it's not defined, as a master password sort of thing. +// I'd go as far as protecting the database credentials that way, but legacy OpenSim needs it unprotected. +// Also keep in mind, people want autostart of their services without having to enter passwords on each boot. + +// TODO - once it is event driven, periodically run things like session clean ups, self healing, and the secure.sh thing. +// And backups off course. + +#include +#ifdef _WIN32 +#include +#else +extern char **environ; +#endif +// Don't overide standard stdio stuff. +#define NO_FCGI_DEFINES +#include +#undef NO_FCGI_DEFINES +//#include "fcgiapp.h" + +#include +#include +#include +#include + +#include "lib/fcgi_SC.h" +#include "lib/handlekeys.h" + +// Both my_config.h and fcgi_config.h define the same PACKAGE* variables, which we don't use anyway, +// I deal with that by using a sed invokation when building fcgi2. + +// https://mariadb.com/kb/en/about-mariadb-connector-c/ Official docs. +// http://dev.mysql.com/doc/refman/5.5/en/c-api-function-overview.html MySQL docs. +// http://zetcode.com/db/mysqlc/ MySQL tutorial. +#include +#include + +// TODO - audit all the alloc()s and free()s involved in qlibc stuff. +#include +#include + +#include +#include "openssl/hmac.h" +#include + +// Toybox's strend overrides another strend that causes MariaDB library to crash. Renaming it to tb_strend helps. +#include "toys.h" + + +GLOBALS( + char *mode; +) + +#define TT this.sledjchisl + +#define FLAG_m 2 + + + + +// Duplicate some small amount of code from qlibc, coz / and = are not good choices, and the standard says we can pick those. +/** + * Encode data using BASE64 algorithm. + * + * @param bin a pointer of input data. + * @param size the length of input data. + * + * @return a malloced string pointer of BASE64 encoded string in case of + * successful, otherwise returns NULL + * + * @code + * const char *text = "hello world"; + * + * char *encstr = qB64_encode(text, strlen(text)); + * if(encstr == NULL) return -1; + * + * printf("Original: %s\n", text); + * printf("Encoded : %s\n", encstr); + * + * size_t decsize = qB64_decode(encstr); + * + * printf("Decoded : %s (%zu bytes)\n", encstr, decsize); + * free(encstr); + * + * --[output]-- + * Original: hello world + * Encoded: aGVsbG8gd29ybGQ= + * Decoded: hello world (11 bytes) + * @endcode + */ +char *qB64_encode(const void *bin, size_t size) { + const char B64CHARTBL[64] = { + 'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P', // 00-0F + 'Q','R','S','T','U','V','W','X','Y','Z','a','b','c','d','e','f', // 10-1F + 'g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v', // 20-2F +// 'w','x','y','z','0','1','2','3','4','5','6','7','8','9','+','/' // 30-3F + 'w','x','y','z','0','1','2','3','4','5','6','7','8','9','+','_' // 30-3F + }; + + if (size == 0) { + return strdup(""); + } + + // malloc for encoded string + char *pszB64 = (char *) malloc( + 4 * ((size / 3) + ((size % 3 == 0) ? 0 : 1)) + 1); + if (pszB64 == NULL) { + return NULL; + } + + char *pszB64Pt = pszB64; + unsigned char *pBinPt, *pBinEnd = (unsigned char *) (bin + size - 1); + unsigned char szIn[3] = { 0, 0, 0 }; + int nOffset; + for (pBinPt = (unsigned char *) bin, nOffset = 0; pBinPt <= pBinEnd; + pBinPt++, nOffset++) { + int nIdxOfThree = nOffset % 3; + szIn[nIdxOfThree] = *pBinPt; + if (nIdxOfThree < 2 && pBinPt < pBinEnd) + continue; + + *pszB64Pt++ = B64CHARTBL[((szIn[0] & 0xFC) >> 2)]; + *pszB64Pt++ = B64CHARTBL[(((szIn[0] & 0x03) << 4) + | ((szIn[1] & 0xF0) >> 4))]; + *pszB64Pt++ = + (nIdxOfThree >= 1) ? + B64CHARTBL[(((szIn[1] & 0x0F) << 2) + | ((szIn[2] & 0xC0) >> 6))] : +// '='; + '-'; +// *pszB64Pt++ = (nIdxOfThree >= 2) ? B64CHARTBL[(szIn[2] & 0x3F)] : '='; + *pszB64Pt++ = (nIdxOfThree >= 2) ? B64CHARTBL[(szIn[2] & 0x3F)] : '-'; + + memset((void *) szIn, 0, sizeof(szIn)); + } + *pszB64Pt = '\0'; + + return pszB64; +} + +/** + * Decode BASE64 encoded string. + * + * @param str a pointer of Base64 encoded string. + * + * @return the length of bytes stored in the str memory in case of successful, + * otherwise returns NULL + * + * @note + * This modify str directly. And the 'str' is always terminated by NULL + * character. + */ +size_t qB64_decode(char *str) { + const char B64MAPTBL[16 * 16] = { + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, // 00-0F + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, // 10-1F + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 62, 64, 64, 64, 63, // 20-2F + 52, 53, 54, 55, 56, 57, 58, 59, 60, 45, 64, 64, 64, 64, 64, 64, // 30-3F + 64, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, // 40-4F + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 64, 64, 64, 64, 64, // 50-5F + 64, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, // 60-6F + 41, 42, 43, 44, 45, 46, 95, 48, 49, 50, 51, 64, 64, 64, 64, 64, // 70-7F + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, // 80-8F + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, // 90-9F + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, // A0-AF + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, // B0-BF + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, // C0-CF + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, // D0-DF + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, // E0-EF + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64 // F0-FF + }; + + char *pEncPt, *pBinPt = str; + int nIdxOfFour = 0; + char cLastByte = 0; + for (pEncPt = str; *pEncPt != '\0'; pEncPt++) { + char cByte = B64MAPTBL[(unsigned char) (*pEncPt)]; + if (cByte == 64) + continue; + + if (nIdxOfFour == 0) { + nIdxOfFour++; + } else if (nIdxOfFour == 1) { + // 00876543 0021???? + //*pBinPt++ = ( ((cLastByte << 2) & 0xFC) | ((cByte >> 4) & 0x03) ); + *pBinPt++ = ((cLastByte << 2) | (cByte >> 4)); + nIdxOfFour++; + } else if (nIdxOfFour == 2) { + // 00??8765 004321?? + //*pBinPt++ = ( ((cLastByte << 4) & 0xF0) | ((cByte >> 2) & 0x0F) ); + *pBinPt++ = ((cLastByte << 4) | (cByte >> 2)); + nIdxOfFour++; + } else { + // 00????87 00654321 + //*pBinPt++ = ( ((cLastByte << 6) & 0xC0) | (cByte & 0x3F) ); + *pBinPt++ = ((cLastByte << 6) | cByte); + nIdxOfFour = 0; + } + + cLastByte = cByte; + } + *pBinPt = '\0'; + + return (pBinPt - str); +} + + + + +// Duplicate some small amount of code from toys/pending/sh.c +int runToy(char *argv[]) +{ + int ret = 0; + struct toy_list *tl; + struct toy_context temp; + sigjmp_buf rebound; + + if ((tl = toy_find(argv[0])) )//&& (tl->flags & (TOYFLAG_NOFORK|TOYFLAG_MAYFORK))) + { + // This fakes lots of what toybox_main() does. + memcpy(&temp, &toys, sizeof(struct toy_context)); + memset(&toys, 0, sizeof(struct toy_context)); + + if (!sigsetjmp(rebound, 1)) + { + toys.rebound = &rebound; + toy_init(tl, argv); // argv must be null terminated + tl->toy_main(); + xflush(0); + } + ret = toys.exitval; + if (toys.optargs != toys.argv+1) free(toys.optargs); + if (toys.old_umask) umask(toys.old_umask); + memcpy(&toys, &temp, sizeof(struct toy_context)); + } + + return ret; +} + + +#undef FALSE +#undef TRUE +#ifndef FALSE +// NEVER change this +typedef enum +{ + FALSE = 0, + TRUE = 1 +} boolean; +#endif + + + +// Silly "man getrandom" is bullshitting. +// Note - this is Linux specific, it's calling a Linux kernel function. +// Remove this when we have a real getrandom(), and replace it with - +// #include +#include +#include +int getrandom(void *b, size_t l, unsigned int f) +{ + return (int) syscall(SYS_getrandom, b, l, f); +} + + + +typedef struct _gridStats gridStats; +struct _gridStats +{ + float next; + struct timeval last; + qhashtbl_t *stats; +}; + + +typedef struct _HTMLfile HTMLfile; +struct _HTMLfile +{ + struct timespec last; + qlist_t *fragments; +}; +qhashtbl_t *HTMLfileCache = NULL; + + +typedef struct _reqData reqData; + + +typedef int (*fieldValidFunc) (reqData *Rd, qhashtbl_t *data); +typedef struct _validFunc validFunc; +struct _validFunc +{ + char *name; + fieldValidFunc func; +}; +qlisttbl_t *fieldValidFuncs = NULL; +static void newValidFunc(char *name, fieldValidFunc func) +{ + validFunc *vf = xmalloc(sizeof(validFunc)); + vf->name = name; vf->func = func; + fieldValidFuncs->put(fieldValidFuncs, vf->name, vf, sizeof(validFunc)); +} + +typedef void *(*pageFunction) (char *file, reqData *Rd, HTMLfile *thisFile); +typedef struct _dynPage dynPage; +struct _dynPage +{ + char *name; + pageFunction func; +}; +qhashtbl_t *dynPages; +static void newDynPage(char *name, pageFunction func) +{ + dynPage *dp = xmalloc(sizeof(dynPage)); + dp->name = name; dp->func = func; + dynPages->put(dynPages, dp->name, dp, sizeof(dynPage)); +} + +typedef void *(*pageBuildFunction) (reqData *Rd, char *message); +typedef struct _buildPage buildPage; +struct _buildPage +{ + char *name; + pageBuildFunction func, eFunc; +}; +qhashtbl_t *buildPages; +static void newBuildPage(char *name, pageBuildFunction func, pageBuildFunction eFunc) +{ + buildPage *bp = xmalloc(sizeof(buildPage)); + bp->name = name; bp->func = func; bp->eFunc = eFunc; + buildPages->put(buildPages, bp->name, bp, sizeof(buildPage)); +} + + +/* TODO - there should be some precedence for values overriding values here. + Nothing official? + https://www.w3.org/standards/webarch/protocols + "This intro text is boilerplate for the beta release of w3.org." Fucking useless. Pffft + https://www.w3.org/Protocols/ + Still nothing official, though the ENV / HEADER stuff tends to be about the protocol things, and cookies / body / queries are about the data things. + +Also including values from the database. + +URL query Values actually provided by the user in the FORM, and other things. +POST body Values actually provided by the user in the FORM. +cookies + https://stackoverflow.com/questions/4056306/how-to-handle-multiple-cookies-with-the-same-name + +headers includes HTTP_COOKIE and QUERY_STRING +env includes headers and HTTP_COOKIE and QUERY_STRING + +database Since all of the above are for updating the database anyway, this goes on the bottom, overridden by all. + Though be wary of security stuff. + +We don't actually get the headers directly, it's all sent via the env. + +http://docs.gantry.org/gantry4/advanced/setby + Says that query overrides cookies, but that might be just for their platform. + +https://framework.zend.com/manual/1.11/en/zend.controller.request.html + Says - "1. GET, 2. POST, 3. COOKIE, 4. SERVER, 5. ENV." + + +Sending cookie headers is a special case, multiples can be sent, otherwise headers are singletons, only send one for each name. + +local storage? Would be client side Javascript thing not usually sent back to server. +*/ + +#define HMACSIZE EVP_MAX_MD_SIZE * 2 +#define HMACSIZE64 88 +typedef struct _sesh sesh; +struct _sesh +{ + char salt[256 + 1], seshID[256 + 1], + sesh[256 + 16 + 10 + 1], munchie[HMACSIZE + 16 + 10 + 1], toke_n_munchie[HMACSIZE + 1], hashish[HMACSIZE + 1], + leaf[HMACSIZE64 + 6 + 1]; + struct timespec timeStamp[2]; + boolean isLinky; +}; + +struct _reqData +{ + lua_State *L; + qhashtbl_t *configs, *queries, *body, *cookies, *headers, *stuff, *database, *Rcookies, *Rheaders; + char *Scheme, *Host, *Method, *Script, *RUri, *doit; + sesh shs, *lnk; + MYSQL *db; + gridStats *stats; + qlist_t *errors, *messages; + qgrow_t *reply; + pageBuildFunction func; + struct timespec then; + boolean chillOut, vegOut; +}; + +static void showSesh(qgrow_t *reply, sesh *shs) +{ + if (shs->isLinky) + reply->addstrf(reply, "Linky:
\n
\n");
+  else
+    reply->addstrf(reply, "Session:
\n
\n");
+
+  reply->addstrf(reply, "   salt = %s\n", shs->salt);
+  reply->addstrf(reply, "   seshID = %s\n", shs->seshID);
+  reply->addstrf(reply, "   timeStamp = %ld.%ld\n", shs->timeStamp[1].tv_sec, shs->timeStamp[1].tv_nsec);
+  reply->addstrf(reply, "   sesh = %s\n", shs->sesh);
+  reply->addstrf(reply, "   munchie = %s\n", shs->munchie);
+  reply->addstrf(reply, "   toke_n_munchie = %s\n", shs->toke_n_munchie);
+  reply->addstrf(reply, "   hashish = %s\n", shs->hashish);
+  reply->addstrf(reply, "   leaf = %s\n", shs->leaf);
+  reply->addstr(reply, "
\n"); +} + + +char toybuf[4096]; +boolean isTmux = 0; +boolean isWeb = 0; +char *pwd = ""; +char *scRoot = "/opt/opensim_SC"; +char *scUser = "opensimsc"; +char *Tconsole = "SledjChisl"; +char *Tsocket = "caches/opensim-tmux.socket"; +char *Ttab = "SC"; +char *Tcmd = "tmux -S"; +char *webRoot = "/opt/opensim_SC/web"; +char *URL = "fcgi-bin/sledjchisl.fcgi"; +int seshTimeOut = 30 * 60; +int idleTimeOut = 24 * 60 * 60; +int newbieTimeOut = 30; +float loadAverageInc = 0.5; +int simTimeOut = 45; +qhashtbl_t *mimeTypes; + + +// TODO - log to file. The problem is we don't know where to log until after we have loaded the configs, and before that we are spewing log messages. +// Now that we are using spawn-fcgi, all the logs are going to STDOUT, which we can capture and write to a file. +// A better idea, when we spawn tmux or spawn-fcgi, capture STDERR, full log everything to that, filtered log to the tmux console (STDOUT). +// Then we can use STDOUT / STDIN to run the console stuff. + +// https://stackoverflow.com/questions/4842424/list-of-ansi-color-escape-sequences +char *logTypes[] = +{ + "91;1;4", "CRITICAL", // red underlined + "31", "ERROR", // dark red + "93", "WARNING", // yellow + "36", "TIMEOUT", // cyan + "97;40", "INFO", // white + "90", "DEBUG", // grey +}; + +#define DATE_TIME_LEN 42 +void logMe(int v, char *format, ...) +{ + va_list va, va2; + int len; + char *ret; + struct timeval tv; + time_t curtime; + char date[DATE_TIME_LEN]; + + va_start(va, format); + va_copy(va2, va); + // How long is it? + len = vsnprintf(0, 0, format, va); + len++; + va_end(va); + // Allocate and do the sprintf() + ret = xmalloc(len); + vsnprintf(ret, len, format, va2); + va_end(va2); + + gettimeofday(&tv, NULL); + curtime = tv.tv_sec; + strftime(date, DATE_TIME_LEN, "(%Z %z) %F %T", localtime(&curtime)); + + v *= 2; + fprintf(stderr, "%s.%.6ld \e[%sm%-8s sledjchisl: %s\e[0m\n", date, tv.tv_usec, logTypes[v], logTypes[v + 1], ret); + free(ret); +} +#define C(...) logMe(0, __VA_ARGS__) +#define E(...) logMe(1, __VA_ARGS__) +#define W(...) logMe(2, __VA_ARGS__) +#define T(...) logMe(3, __VA_ARGS__) +#define I(...) logMe(4, __VA_ARGS__) +#define D(...) logMe(5, __VA_ARGS__) + + +static void addStrL(qlist_t *list, char *s) +{ + list->addlast(list, s, strlen(s) + 1); +} + +static char *getStrH(qhashtbl_t *hash, char *key) +{ + char *ret = "", *t; + + t = hash->getstr(hash, key, false); + if (NULL != t) + ret = t; + return ret; +} + + +char *myHMAC(char *in, boolean b64) +{ + EVP_MD_CTX *mdctx = EVP_MD_CTX_create(); // Gets renamed to EVP_MD_CTX_new() in later versions. + unsigned char md_value[EVP_MAX_MD_SIZE]; + unsigned int md_len; + + EVP_DigestInit_ex(mdctx, EVP_sha512(), NULL); // EVP_sha3_512() isn't available until later versions. + EVP_DigestUpdate(mdctx, in, strlen(in)); + EVP_DigestFinal_ex(mdctx, md_value, &md_len); + EVP_MD_CTX_destroy(mdctx); // Gets renamed to EVP_MD_CTX_free() in later versions. + + if (b64) + return qB64_encode(md_value, md_len); + else + return qhex_encode(md_value, md_len); +} + +char *myHMACkey(char *key, char *in, boolean b64) +{ + unsigned char md_value[EVP_MAX_MD_SIZE]; + unsigned int md_len; + unsigned char* digest = HMAC(EVP_sha512(), key, strlen(key), (unsigned char *) in, strlen(in), md_value, &md_len); + + if (b64) + return qB64_encode(md_value, md_len); + else + return qhex_encode(md_value, md_len); +} + + +// In Lua 5.0 reference manual is a table traversal example at page 29. +void PrintTable(lua_State *L) +{ + lua_pushnil(L); + + while (lua_next(L, -2) != 0) + { + // Numbers can convert to strings, so check for numbers before checking for strings. + if (lua_isnumber(L, -1)) + printf("%s = %f\n", lua_tostring(L, -2), lua_tonumber(L, -1)); + else if (lua_isstring(L, -1)) + printf("%s = '%s'\n", lua_tostring(L, -2), lua_tostring(L, -1)); + else if (lua_istable(L, -1)) + PrintTable(L); + lua_pop(L, 1); + } +} + + +int sendTmuxKeys(char *dest, char *keys) +{ + int ret = 0, i; + char *c = xmprintf("%s %s/%s send-keys -t %s:%s '%s'", Tcmd, scRoot, Tsocket, Tconsole, dest, keys); + + i = system(c); + if (!WIFEXITED(i)) + E("tmux send-keys command failed!"); + free(c); + return ret; +} + +int sendTmuxCmd(char *dest, char *cmd) +{ + int ret = 0, i; + char *c = xmprintf("%s %s/%s send-keys -t %s:'%s' '%s' Enter", Tcmd, scRoot, Tsocket, Tconsole, dest, cmd); + + i = system(c); + if (!WIFEXITED(i)) + E("tmux send-keys command failed!"); + free(c); + return ret; +} + +void waitTmuxText(char *dest, char *text) +{ + int i; + char *c = xmprintf("sleep 5; %s %s/%s capture-pane -t %s:'%s' -p | grep -E '%s' 2>&1 > /dev/null", Tcmd, scRoot, Tsocket, Tconsole, dest, text); + + D("Waiting for '%s'.", text); + do + { + i = system(c); + if (!WIFEXITED(i)) + { + E("tmux capture-pane command failed!"); + break; + } + else if (0 == WEXITSTATUS(i)) + break; + } while (1); + + free(c); +} + +float waitLoadAverage(float la, float extra, int timeout) +{ + struct sysinfo info; + struct timespec timeOut; + float l; + int to = timeout; + + T("Sleeping until load average is below %.02f (%.02f + %.02f) or for %d seconds.", la + extra, la, extra, timeout); + clock_gettime(CLOCK_MONOTONIC, &timeOut); + to += timeOut.tv_sec; + + do + { + msleep(5000); + sysinfo(&info); + l = info.loads[0]/65536.0; + clock_gettime(CLOCK_MONOTONIC, &timeOut); + timeout -= 5; + T("Tick, load average is %.02f, countdown %d seconds.", l, timeout); + } while (((la + extra) < l) && (timeOut.tv_sec < to)); + + return l; +} + + +// Rob forget to do this, but at least he didn't declare it static. +struct dirtree *dirtree_handle_callback(struct dirtree *new, int (*callback)(struct dirtree *node)); + +typedef struct _simList simList; +struct _simList +{ + int len, num; + char **sims; +}; + +static int filterSims(struct dirtree *node) +{ + if (!node->parent) return DIRTREE_RECURSE | DIRTREE_SHUTUP; + if ((strncmp(node->name, "sim", 3) == 0) && ((strcmp(node->name, "sim_skeleton") != 0))) + { + simList *list = (simList *) node->parent->extra; + + if ((list->num + 1) > list->len) + { + list->len = list->len + 1; + list->sims = xrealloc(list->sims, list->len * sizeof(char *)); + } + list->sims[list->num] = xstrdup(node->name); + list->num++; + } + return 0; +} + +simList *getSims() +{ + simList *sims = xmalloc(sizeof(simList)); + memset(sims, 0, sizeof(simList)); + char *path = xmprintf("%s/config", scRoot); + struct dirtree *new = dirtree_add_node(0, path, 0); + new->extra = (long) sims; + dirtree_handle_callback(new, filterSims); + qsort(sims->sims, sims->num, sizeof(char *), qstrcmp); + free(path); + return sims; +} + + +static int filterInis(struct dirtree *node) +{ + if (!node->parent) return DIRTREE_RECURSE | DIRTREE_SHUTUP; + int l = strlen(node->name); + if (strncmp(&(node->name[l - 4]), ".ini", 4) == 0) + { + strcpy((char *) node->parent->extra, xstrdup(node->name)); + return DIRTREE_ABORT; + } + return 0; +} + +char *getSimName(char *sim) +{ + char *ret = NULL; + char *c = xmprintf("%s/config/%s", scRoot, sim); + struct dirtree *new = dirtree_add_node(0, c, 0); + + free(c); + c = xzalloc(1024); + new->extra = (long) c; + dirtree_handle_callback(new, filterInis); + if ('\0' != c[0]) + { + char *temp = NULL; + regex_t pat; + regmatch_t m[2]; + long len; + int fd; + + temp = xmprintf("%s/config/%s/%s", scRoot, sim, c); + fd = xopenro(temp); + free(temp); + xregcomp(&pat, "RegionName = \"(.+)\"", REG_EXTENDED); + do + { + // TODO - get_line() is slow, and wont help much with DOS and Mac line endings. + temp = get_line(fd); + if (temp) + { + if (!regexec(&pat, temp, 2, m, 0)) + { + // Return first parenthesized subexpression as string. + if (pat.re_nsub > 0) + { + ret = xmprintf("%.*s", (int) (m[1].rm_eo - m[1].rm_so), temp + m[1].rm_so); + break; + } + } + } + } while (temp); + xclose(fd); + } + free(c); + return ret; +} + + +// Expects either "simXX" or "ROBUST". +// TODO - ROBUST isn't creating it's pid file, bug in ROBUST, it has config for it. +int checkSimIsRunning(char *sim) +{ + int ret = 0; + struct stat st; + + // Check if it's running. + char *path = xmprintf("%s/caches/%s.pid", scRoot, sim); + if (0 == stat(path, &st)) + { + int fd, i; + char *pid = NULL; + + // Double check if it's REALLY running. + if ((fd = xopenro(path)) == -1) + perror_msg("xopenro(%s)", path); + else + { + pid = get_line(fd); + if (NULL == pid) + perror_msg("get_line(%s)", path); + else + { + xclose(fd); + +// I'd rather re-use the toysh command running stuff, since ps is a toy, but that's private. +// TODO - switch to toybox ps and rm. + free(path); + path = xmprintf("ps -p %s --no-headers -o comm", pid); + i = system(path); + if (WIFEXITED(i)) + { + if (0 != WEXITSTATUS(i)) // No such pid. + { + free(path); + path = xmprintf("rm -f %s/caches/%s.pid", scRoot, sim); + D("%s", path); + i = system(path); + } + } + } + } + } + + // Now check if it's really really running. lol + free(path); + path = xmprintf("%s/caches/%s.pid", scRoot, sim); + if (0 == stat(path, &st)) + ret = 1; + + free(path); + return ret; +} + + +static void PrintEnv(qgrow_t *reply, char *label, char **envp) +{ + reply->addstrf(reply, "%s:
\n
\n", label);
+  for ( ; *envp != NULL; envp++)
+    reply->addstrf(reply, "%s\n", *envp);
+  reply->addstr(reply, "
\n"); +} + +static void printEnv(char **envp) +{ + for ( ; *envp != NULL; envp++) + D("%s", *envp); +} + + +typedef struct _rowData rowData; +struct _rowData +{ + char **fieldNames; + qlist_t *rows; +}; + +static void dumpHash(qhashtbl_t *tbl) +{ + qhashtbl_obj_t obj; + + memset((void*)&obj, 0, sizeof(obj)); + tbl->lock(tbl); + while(tbl->getnext(tbl, &obj, true) == true) + D("%s = %s", obj.name, (char *) obj.data); + tbl->unlock(tbl); +} + +static void dumpArray(int d, char **ar) +{ + int i = 0; + + while (ar[i] != NULL) + { + D("%d %d %s", d, i, ar[i]); + i++; + } +} + + +/* How to deal with prepared SQL statements. +http://karlssonondatabases.blogspot.com/2010/07/prepared-statements-are-they-useful-or.html +https://blog.cotten.io/a-taste-of-mysql-in-c-87c5de84a31d?gi=ab3dd1425b29 +https://raspberry-projects.com/pi/programming-in-c/databases-programming-in-c/mysql/accessing-the-database + +IG and CG now both have sims connected to other grids, so some sort of +multi database solution would be good, then we can run the grid and the +external sims all in one. + +Not sure if this'll work with Count(*). + +--------------------------------------------- + +The complicated bit is the binds. + +You are binding field values to C memory locations. +The parameters and returned fields need binds. +Mostly seems to be the value parts of the SQL statements. + +I suspect most will be of the form - + ... WHERE x=? and foo=? + INSERT INTO table VALUES (?,?,?) + UPDATE table SET x=?, foo=? WHERE id=? + + A multi table update - + UPDATE items,month SET items.price=month.price WHERE items.id=month.id; +*/ + +typedef struct _dbField dbField; +struct _dbField +{ + char *name; + enum enum_field_types type; + unsigned long length; + unsigned int flags; + unsigned int decimals; +}; + +qlisttbl_t *dbGetFields(MYSQL *db, char *table) +{ + static qhashtbl_t *tables = NULL; + if (NULL == tables) tables = qhashtbl(0, 0); + qlisttbl_t *ret = tables->get(tables, table, NULL, false); + + if (NULL == ret) + { + // Seems the only way to get field metadata is to actually perform a SQL statement, then you get the field metadata for the result set. + // Chicken, meet egg, sorry you had to cross the road for this. + char *sql = xmprintf("SELECT * FROM %s LIMIT 0", table); + +D("Getting field metadata for %s", table); + if (mysql_query(db, sql)) + E("Query failed: %s\n%s", mysql_error(db), sql); + else + { + MYSQL_RES *res = mysql_store_result(db); + + if (!res) + E("Couldn't get results set from %s\n %s", mysql_error(db), sql); + else + { + MYSQL_FIELD *fields = mysql_fetch_fields(res); + + if (!fields) + E("Failed fetching fields: %s", mysql_error(db)); + else + { + unsigned int i, num_fields = mysql_num_fields(res); + + ret = qlisttbl(QLISTTBL_UNIQUE | QLISTTBL_LOOKUPFORWARD); + for (i = 0; i < num_fields; i++) + { + dbField *fld = xmalloc(sizeof(dbField)); + fld->name = xstrdup(fields[i].name); + fld->type = fields[i].type; + fld->length = fields[i].length; + fld->flags = fields[i].flags; + fld->decimals = fields[i].decimals; + ret->put(ret, fld->name, fld, sizeof(*fld)); + } + tables->put(tables, table, ret, sizeof(*ret)); + } + mysql_free_result(res); + } + } + free(sql); + } + + return ret; +} + +void dbFreeFields(qlisttbl_t *flds) +{ + qlisttbl_obj_t obj; + memset((void *) &obj, 0, sizeof(obj)); + flds->lock(flds); + while(flds->getnext(flds, &obj, NULL, false) == true) + { + dbField *fld = (dbField *) obj.data; + free(fld->name); + } + flds->unlock(flds); + flds->free(flds); +} + +typedef struct _dbRequest dbRequest; +struct _dbRequest +{ + MYSQL *db; + char *table, *join, *where, *order, *sql; + qlisttbl_t *flds; + int inCount, outCount, rowCount; + char **inParams, **outParams; + MYSQL_STMT *prep; // NOTE - executing it stores state in this. + MYSQL_BIND *inBind, *outBind; + rowData *rows; + my_ulonglong count; + boolean freeOutParams; +}; + +void dbDoSomething(dbRequest *req, boolean count, ...) +{ + va_list ap; + struct timespec now, then; + int i, j; + MYSQL_RES *prepare_meta_result = NULL; + + if (-1 == clock_gettime(CLOCK_REALTIME, &then)) + perror_msg("Unable to get the time."); + + va_start(ap, count); + + if (NULL == req->prep) + { + req->flds = dbGetFields(req->db, req->table); + if (NULL == req->flds) + { + E("Unknown fields for table %s.", req->table); + goto end; + } + + char *select = xmprintf(""); + i = 0; + while (req->outParams[i] != NULL) + { + char *t = xmprintf("%s,%s", select, req->outParams[i]); + free(select); + select = t; + i++; + } + if (0 == i) + { + if (count) + select = xmprintf(",Count(*)"); + else + select = xmprintf(",*"); + } + + if (NULL == req->join) + req->join = ""; + + if (req->where) + req->sql = xmprintf("SELECT %s FROM %s %s WHERE %s", &select[1], req->table, req->join, req->where); + else + req->sql = xmprintf("SELECT %s FROM %s", &select[1], req->table, req->join); + free(select); + if (req->order) + { + char *t = xmprintf("%s ORDER BY %s", req->sql, req->order); + + free(req->sql); + req->sql = t; + } + +D("New SQL statement - %s", req->sql); + // prepare statement with the other fields + req->prep = mysql_stmt_init(req->db); + if (NULL == req->prep) + { + E("Statement prepare init failed: %s\n", mysql_stmt_error(req->prep)); + goto end; + } + if (mysql_stmt_prepare(req->prep, req->sql, strlen(req->sql))) + { + E("Statement prepare failed: %s\n", mysql_stmt_error(req->prep)); + goto end; + } + + // setup the bind stuff for any "?" parameters in the SQL. + req->inCount = mysql_stmt_param_count(req->prep); + i = 0; + while (req->inParams[i] != NULL) + i++; + if (i != req->inCount) + { + E("In parameters count don't match %d != %d for - %s", i, req->inCount, req->sql); + goto freeIt; + } + req->inBind = xzalloc(i * sizeof(MYSQL_BIND)); + for (i = 0; i < req->inCount; i++) + { + dbField *fld = req->flds->get(req->flds, req->inParams[i], NULL, false); + + if (NULL == fld) + { + E("Unknown input field %d %s.%s for - %s", i, req->table, req->inParams[i], req->sql); + goto freeIt; + } + else + { + // https://blog.cotten.io/a-taste-of-mysql-in-c-87c5de84a31d?gi=ab3dd1425b29 + // For some gotchas about all of this binding bit. + req->inBind[i].buffer_type = fld->type; + req->inBind[i].buffer = xzalloc(fld->length) + 1; // Note the + 1 is for string types, and a waste for the rest. + req->inBind[i].buffer_length = fld->length; + switch(fld->type) + { + case MYSQL_TYPE_TINY: + { + break; + } + + case MYSQL_TYPE_SHORT: + { + req->inBind[i].is_unsigned = FALSE; + break; + } + + case MYSQL_TYPE_INT24: + { + req->inBind[i].is_unsigned = FALSE; + break; + } + + case MYSQL_TYPE_LONG: + { + req->inBind[i].is_unsigned = FALSE; + break; + } + + case MYSQL_TYPE_LONGLONG: + { + req->inBind[i].is_unsigned = FALSE; + break; + } + + case MYSQL_TYPE_FLOAT: + { + break; + } + + case MYSQL_TYPE_DOUBLE: + { + break; + } + + case MYSQL_TYPE_NEWDECIMAL: + { + break; + } + + case MYSQL_TYPE_TIME: + case MYSQL_TYPE_DATE: + case MYSQL_TYPE_DATETIME: + case MYSQL_TYPE_TIMESTAMP: + { + break; + } + + case MYSQL_TYPE_STRING: + case MYSQL_TYPE_VAR_STRING: + { + req->inBind[i].is_null = xzalloc(sizeof(my_bool)); + req->inBind[i].length = xzalloc(sizeof(unsigned long)); + break; + } + + case MYSQL_TYPE_TINY_BLOB: + case MYSQL_TYPE_BLOB: + case MYSQL_TYPE_MEDIUM_BLOB: + case MYSQL_TYPE_LONG_BLOB: + { + req->inBind[i].is_null = xzalloc(sizeof(my_bool)); + break; + } + + case MYSQL_TYPE_BIT: + { + req->inBind[i].is_null = xzalloc(sizeof(my_bool)); + break; + } + + case MYSQL_TYPE_NULL: + { + break; + } + } + } + } + +// TODO - if this is not a count, setup result bind paramateres, may be needed for counts as well. + prepare_meta_result = mysql_stmt_result_metadata(req->prep); + if (!prepare_meta_result) + { + D(" mysql_stmt_result_metadata(), returned no meta information - %s\n", mysql_stmt_error(req->prep)); + goto freeIt; + } + + if (count) + { +I("count!!!!!!!!!!!!!!!!"); + } + else + { + req->outCount = mysql_num_fields(prepare_meta_result); + i = 0; + while (req->outParams[i] != NULL) + i++; + if (0 == i) // Passing in {NULL} as req->outParams means "return all of them". + { + req->outParams = xzalloc((req->outCount + 1) * sizeof(char *)); + req->freeOutParams = TRUE; + qlisttbl_obj_t obj; + memset((void*)&obj, 0, sizeof(obj)); + req->flds->lock(req->flds); + while (req->flds->getnext(req->flds, &obj, NULL, false) == true) + { + dbField *fld = (dbField *) obj.data; + req->outParams[i] = fld->name; + i++; + } + req->outParams[i] = NULL; + req->flds->unlock(req->flds); + } + if (i != req->outCount) + { + E("Out parameters count doesn't match %d != %d foqr - %s", i, req->outCount, req->sql); + goto freeIt; + } + req->outBind = xzalloc(i * sizeof(MYSQL_BIND)); + for (i = 0; i < req->outCount; i++) + { + dbField *fld = req->flds->get(req->flds, req->outParams[i], NULL, false); + + if (NULL == fld) + { + E("Unknown output field %d %s.%s foqr - %s", i, req->table, req->outParams[i], req->sql); + goto freeIt; + } + else + { + // https://blog.cotten.io/a-taste-of-mysql-in-c-87c5de84a31d?gi=ab3dd1425b29 + // For some gotchas about all of this binding bit. + req->outBind[i].buffer_type = fld->type; + req->outBind[i].buffer = xzalloc(fld->length + 1); // Note the + 1 is for string types, and a waste for the rest. + req->outBind[i].buffer_length = fld->length; + req->outBind[i].error = xzalloc(sizeof(my_bool)); + req->outBind[i].is_null = xzalloc(sizeof(my_bool)); + switch(fld->type) + { + case MYSQL_TYPE_TINY: + { +//D("TINY %d %s %d", i, fld->name, req->outBind[i].buffer_length); + break; + } + + case MYSQL_TYPE_SHORT: + { +//D("SHORT %s %d", fld->name, req->outBind[i].buffer_length); + req->outBind[i].is_unsigned = FALSE; + break; + } + + case MYSQL_TYPE_INT24: + { +//D("INT24 %s %d", fld->name, req->outBind[i].buffer_length); + req->outBind[i].is_unsigned = FALSE; + break; + } + + case MYSQL_TYPE_LONG: + { +//D("LONG %d %s %d", i, fld->name, req->outBind[i].buffer_length); + req->outBind[i].is_unsigned = FALSE; + break; + } + + case MYSQL_TYPE_LONGLONG: + { +//D("LONG LONG %s %d", fld->name, req->outBind[i].buffer_length); + req->outBind[i].is_unsigned = FALSE; + break; + } + + case MYSQL_TYPE_FLOAT: + { +//D("FLOAT %s %d", fld->name, req->outBind[i].buffer_length); + break; + } + + case MYSQL_TYPE_DOUBLE: + { +//D("DOUBLE %s %d", fld->name, req->outBind[i].buffer_length); + break; + } + + case MYSQL_TYPE_NEWDECIMAL: + { +//D("NEWDECIMAL %s %d", fld->name, req->outBind[i].buffer_length); + break; + } + + case MYSQL_TYPE_TIME: + case MYSQL_TYPE_DATE: + case MYSQL_TYPE_DATETIME: + case MYSQL_TYPE_TIMESTAMP: + { +//D("DATETIME %s %d", fld->name, req->outBind[i].buffer_length); + break; + } + + case MYSQL_TYPE_STRING: + case MYSQL_TYPE_VAR_STRING: + { +//D("STRING %s %d", fld->name, req->outBind[i].buffer_length); + req->outBind[i].length = xzalloc(sizeof(unsigned long)); + break; + } + + case MYSQL_TYPE_TINY_BLOB: + case MYSQL_TYPE_BLOB: + case MYSQL_TYPE_MEDIUM_BLOB: + case MYSQL_TYPE_LONG_BLOB: + { +//D("BLOB %s %d", fld->name, req->outBind[i].buffer_length); + break; + } + + case MYSQL_TYPE_BIT: + { +//D("BIT %s %d", fld->name, req->outBind[i].buffer_length); + break; + } + + case MYSQL_TYPE_NULL: + { +//D("NULL %s %d", fld->name, req->outBind[i].buffer_length); + break; + } + } + } + } + if (mysql_stmt_bind_result(req->prep, req->outBind)) + { + E("Bind failed."); + goto freeIt; + } + } + } + + +//D("input bind for %s", req->sql); + for (i = 0; i < req->inCount; i++) + { + dbField *fld = req->flds->get(req->flds, req->inParams[i], NULL, false); + + if (NULL == fld) + { + E("Unknown input field %s.%s for - %s", req->table, req->inParams[i], req->sql); + goto freeIt; + } + else + { + switch(fld->type) + { + case MYSQL_TYPE_TINY: + { + int c = va_arg(ap, int); + signed char d = (signed char) c; + + memcpy(&d, req->inBind[i].buffer, (size_t) fld->length); + break; + } + + case MYSQL_TYPE_SHORT: + { + int c = va_arg(ap, int); + short int d = (short int) c; + + memcpy(&d, req->inBind[i].buffer, (size_t) fld->length); + break; + } + + case MYSQL_TYPE_INT24: + { + int d = va_arg(ap, int); + + memcpy(&d, req->inBind[i].buffer, (size_t) fld->length); + break; + } + + case MYSQL_TYPE_LONG: + { + long d = va_arg(ap, long); + + memcpy(&d, req->inBind[i].buffer, (size_t) fld->length); + break; + } + + case MYSQL_TYPE_LONGLONG: + { + long long int d = va_arg(ap, long long int); + + memcpy(&d, req->inBind[i].buffer, (size_t) fld->length); + break; + } + + case MYSQL_TYPE_FLOAT: + { + double c = va_arg(ap, double); + float d = (float) c; + + memcpy(&d, req->inBind[i].buffer, (size_t) fld->length); + break; + } + + case MYSQL_TYPE_DOUBLE: + { + double d = va_arg(ap, double); + + memcpy(&d, req->inBind[i].buffer, (size_t) fld->length); + break; + } + + case MYSQL_TYPE_NEWDECIMAL: + { + break; + } + + case MYSQL_TYPE_TIME: + case MYSQL_TYPE_DATE: + case MYSQL_TYPE_DATETIME: + case MYSQL_TYPE_TIMESTAMP: + { + MYSQL_TIME d = va_arg(ap, MYSQL_TIME); + + memcpy(&d, req->inBind[i].buffer, (size_t) fld->length); + break; + } + + case MYSQL_TYPE_STRING: + case MYSQL_TYPE_VAR_STRING: + { + char *d = va_arg(ap, char *); + unsigned long l = strlen(d); + + if (l > fld->length) + l = fld->length; + *(req->inBind[i].length) = l; + strncpy(req->inBind[i].buffer, d, (size_t) l); + ((char *) req->inBind[i].buffer)[l] = '\0'; + break; + } + + case MYSQL_TYPE_TINY_BLOB: + case MYSQL_TYPE_BLOB: + case MYSQL_TYPE_MEDIUM_BLOB: + case MYSQL_TYPE_LONG_BLOB: + { + break; + } + + case MYSQL_TYPE_BIT: + { + break; + } + + case MYSQL_TYPE_NULL: + { + break; + } + } + } + } + if (mysql_stmt_bind_param(req->prep, req->inBind)) + { + E("Bind failed."); + goto freeIt; + } + + +D("Execute %s", req->sql); + + // do the prepared statement req->prep. + if (mysql_stmt_execute(req->prep)) + { + E("Statement execute failed: %s\n", mysql_stmt_error(req->prep)); + goto freeIt; + } + + int fs = mysql_stmt_field_count(req->prep); + // stuff results back into req. + if (NULL != req->outBind) + { + req->rows = xmalloc(sizeof(rowData)); + req->rows->fieldNames = xzalloc(fs * sizeof(char *)); + if (mysql_stmt_store_result(req->prep)) + { + E(" mysql_stmt_store_result() failed %s", mysql_stmt_error(req->prep)); + goto freeIt; + } + req->rowCount = mysql_stmt_num_rows(req->prep); + if (0 == req->rowCount) + D("No rows returned from : %s\n", req->sql); + else + D("%d rows of %d fields returned from : %s\n", req->rowCount, fs, req->sql); + + req->rows->rows = qlist(0); + while (MYSQL_NO_DATA != mysql_stmt_fetch(req->prep)) + { + qhashtbl_t *flds = qhashtbl(0, 0); + + for (i = 0; i < req->outCount; i++) + { + dbField *fld = req->flds->get(req->flds, req->outParams[i], NULL, false); + + req->rows->fieldNames[i] = fld->name; + if (!*(req->outBind[i].is_null)) + { +//D("2.8 %s", req->rows->fieldNames[i]); + flds->put(flds, req->rows->fieldNames[i], req->outBind[i].buffer, req->outBind[i].buffer_length); + + switch(fld->type) + { + case MYSQL_TYPE_TINY: + { + break; + } + + case MYSQL_TYPE_SHORT: + { + char *t = xmprintf("%d", (int) *((int *) req->outBind[i].buffer)); + flds->putstr(flds, req->rows->fieldNames[i], t); + break; + } + + case MYSQL_TYPE_INT24: + { + char *t = xmprintf("%d", (int) *((int *) req->outBind[i].buffer)); + flds->putstr(flds, req->rows->fieldNames[i], t); + break; + } + + case MYSQL_TYPE_LONG: + { + if (NULL == req->outBind[i].buffer) + { + E("Field %d %s is NULL", i, fld->name); + goto freeIt; + } + char *t = xmprintf("%d", (int) *((int *) (req->outBind[i].buffer))); +//D("Setting %i %s %s", i, fld->name, t); + flds->putstr(flds, req->rows->fieldNames[i], t); + break; + } + + case MYSQL_TYPE_LONGLONG: + { + char *t = xmprintf("%d", (int) *((int *) req->outBind[i].buffer)); + flds->putstr(flds, req->rows->fieldNames[i], t); + break; + } + + case MYSQL_TYPE_FLOAT: + { + break; + } + + case MYSQL_TYPE_DOUBLE: + { + break; + } + + case MYSQL_TYPE_NEWDECIMAL: + { + break; + } + + case MYSQL_TYPE_TIME: + case MYSQL_TYPE_DATE: + case MYSQL_TYPE_DATETIME: + case MYSQL_TYPE_TIMESTAMP: + { + break; + } + + case MYSQL_TYPE_STRING: + case MYSQL_TYPE_VAR_STRING: + { + break; + } + + case MYSQL_TYPE_TINY_BLOB: + case MYSQL_TYPE_BLOB: + case MYSQL_TYPE_MEDIUM_BLOB: + case MYSQL_TYPE_LONG_BLOB: + { + break; + } + + case MYSQL_TYPE_BIT: + { + break; + } + + case MYSQL_TYPE_NULL: + { + break; + } + } + } + else + { + D("Not setting data %s, coz it's NULL", fld->name); + } + } + req->rows->rows->addlast(req->rows->rows, flds, sizeof(*flds)); + } + } + +freeIt: + if (prepare_meta_result) + mysql_free_result(prepare_meta_result); + if (mysql_stmt_free_result(req->prep)) + E("Statement result freeing failed: %s\n", mysql_stmt_error(req->prep)); + +end: + va_end(ap); + + if (-1 == clock_gettime(CLOCK_REALTIME, &now)) + perror_msg("Unable to get the time."); + double n = (now.tv_sec * 1000000000.0) + now.tv_nsec; + double t = (then.tv_sec * 1000000000.0) + then.tv_nsec; + T("dbDoSomething(%s) took %lf seconds", req->sql, (n - t) / 1000000000.0); + return; +} + +// Copy the SQL results into the request structure. +void dbPull(reqData *Rd, char *table, rowData *rows) +{ + char *where; + qhashtbl_t *me = rows->rows->popfirst(rows->rows, NULL); + qhashtbl_obj_t obj; + + memset((void*)&obj, 0, sizeof(obj)); + me->lock(me); + while(me->getnext(me, &obj, true) == true) + { + where = xmprintf("%s.%s", table, obj.name); + Rd->database->putstr(Rd->database, where, (char *) obj.data); + me->remove(me, obj.name); + free(where); + } + me->unlock(me); + free(me); +} + +/* +void dbFreeRequest(dbRequest *req) +{ + int i; + + if (NULL != req->outBind) + { + for (i = 0; i < req->outCount; i++) + { + if (NULL != req->outBind[i].buffer) free(req->outBind[i].buffer); + if (NULL != req->outBind[i].length) free(req->outBind[i].length); + if (NULL != req->outBind[i].error) free(req->outBind[i].error); + if (NULL != req->outBind[i].is_null) free(req->outBind[i].is_null); + } + free(req->outBind); + } + if (NULL != req->inBind) + { + for (i = 0; i < req->inCount; i++) + { + if (NULL != req->inBind[i].buffer) free(req->inBind[i].buffer); + if (NULL != req->inBind[i].length) free(req->inBind[i].length); + if (NULL != req->inBind[i].error) free(req->inBind[i].error); + if (NULL != req->inBind[i].is_null) free(req->inBind[i].is_null); + } + free(req->inBind); + } + + if (req->freeOutParams) free(req->outParams); + if (NULL != req->sql) free(req->sql); + if (NULL != req->prep) mysql_stmt_close(req->prep); +} +*/ + +my_ulonglong dbCount(MYSQL *db, char *table, char *where) +{ + my_ulonglong ret = 0; + char *sql; + struct timespec now, then; + + if (-1 == clock_gettime(CLOCK_REALTIME, &then)) + perror_msg("Unable to get the time."); + + if (where) + sql = xmprintf("SELECT Count(*) FROM %s WHERE %s", table, where); + else + sql = xmprintf("SELECT Count(*) FROM %s", table); + + if (mysql_query(db, sql)) + E("Query failed: %s", mysql_error(db)); + else + { + MYSQL_RES *result = mysql_store_result(db); + + if (!result) + E("Couldn't get results set from %s\n: %s", sql, mysql_error(db)); + else + { + MYSQL_ROW row = mysql_fetch_row(result); + if (!row) + E("Couldn't get row from %s\n: %s", sql, mysql_error(db)); + else + ret = atoll(row[0]); + mysql_free_result(result); + } + } + + if (-1 == clock_gettime(CLOCK_REALTIME, &now)) + perror_msg("Unable to get the time."); + double n = (now.tv_sec * 1000000000.0) + now.tv_nsec; + double t = (then.tv_sec * 1000000000.0) + then.tv_nsec; + T("dbCount(%s) took %lf seconds", sql, (n - t) / 1000000000.0); + free(sql); + return ret; +} + +my_ulonglong dbCountJoin(MYSQL *db, char *table, char *select, char *join, char *where) +{ + my_ulonglong ret = 0; + char *sql; + struct timespec now, then; + + if (-1 == clock_gettime(CLOCK_REALTIME, &then)) + perror_msg("Unable to get the time."); + + if (NULL == select) + select = "*"; + if (NULL == join) + join = ""; + + if (where) + sql = xmprintf("SELECT %s FROM %s %s WHERE %s", select, table, join, where); + else + sql = xmprintf("SELECT %s FROM %s", select, table, join); + + if (mysql_query(db, sql)) + E("Query failed: %s", mysql_error(db)); + else + { + MYSQL_RES *result = mysql_store_result(db); + + if (!result) + E("Couldn't get results set from %s\n: %s", sql, mysql_error(db)); + else + ret = mysql_num_rows(result); + mysql_free_result(result); + } + + if (-1 == clock_gettime(CLOCK_REALTIME, &now)) + perror_msg("Unable to get the time."); + double n = (now.tv_sec * 1000000000.0) + now.tv_nsec; + double t = (then.tv_sec * 1000000000.0) + then.tv_nsec; + T("dbCointJoin(%s) took %lf seconds", sql, (n - t) / 1000000000.0); + free(sql); + return ret; +} + +MYSQL_RES *dbSelect(MYSQL *db, char *table, char *select, char *join, char *where, char *order) +{ + MYSQL_RES *ret = NULL; + char *sql; + struct timespec now, then; + + if (-1 == clock_gettime(CLOCK_REALTIME, &then)) + perror_msg("Unable to get the time."); + + if (NULL == select) + select = "*"; + if (NULL == join) + join = ""; + + if (where) + sql = xmprintf("SELECT %s FROM %s %s WHERE %s", select, table, join, where); + else + sql = xmprintf("SELECT %s FROM %s", select, table, join); + + if (order) + { + char *t = xmprintf("%s ORDER BY %s", sql, order); + + free(sql); + sql = t; + } + + if (mysql_query(db, sql)) + E("Query failed: %s\n%s", mysql_error(db), sql); + else + { + ret = mysql_store_result(db); + if (!ret) + E("Couldn't get results set from %s\n %s", mysql_error(db), sql); + } + + if (-1 == clock_gettime(CLOCK_REALTIME, &now)) + perror_msg("Unable to get the time."); + double n = (now.tv_sec * 1000000000.0) + now.tv_nsec; + double t = (then.tv_sec * 1000000000.0) + then.tv_nsec; + T("dbSelect(%s) took %lf seconds", sql, (n - t) / 1000000000.0); + free(sql); + return ret; +} + + +void replaceStr(qhashtbl_t *ssi, char *key, char *value) +{ + ssi->putstr(ssi, key, value); +} + +void replaceLong(qhashtbl_t *ssi, char *key, my_ulonglong value) +{ + char *tmp = xmprintf("%lu", value); + + replaceStr(ssi, key, tmp); + free(tmp); +} + + +float timeDiff(struct timeval *now, struct timeval *then) +{ + if (0 == gettimeofday(now, NULL)) + { + struct timeval thisTime = { 0, 0 }; + double result = 0.0; + + thisTime.tv_sec = now->tv_sec; + thisTime.tv_usec = now->tv_usec; + if (thisTime.tv_usec < then->tv_usec) + { + thisTime.tv_sec--; + thisTime.tv_usec += 1000000; + } + thisTime.tv_usec -= then->tv_usec; + thisTime.tv_sec -= then->tv_sec; + result = ((double) thisTime.tv_usec) / ((double) 1000000.0); + result += thisTime.tv_sec; + return result; + } + + return 0.0; +} + + +gridStats *getStats(MYSQL *db, gridStats *stats) +{ + if (NULL == stats) + { + stats = xmalloc(sizeof(gridStats)); + stats->next = 300; + gettimeofday(&(stats->last), NULL); + stats->stats = qhashtbl(0, 0); + stats->stats->putstr(stats->stats, "version", "SledjChisl FCGI Dev 0.1"); + stats->stats->putstr(stats->stats, "grid", "my grid"); + stats->stats->putstr(stats->stats, "uri", "http://localhost:8002/"); +// TODO - figure out how to do this. Do this once ROBUST is fixed to actually store it's PID - +// if (checkSimIsRunning("ROBUST")) + stats->stats->putstr(stats->stats, "gridOnline", "??"); + } + else + { + static struct timeval thisTime; + if (stats->next > timeDiff(&thisTime, &(stats->last))) + return stats; + } + + if (db) + { + I("Getting fresh grid stats."); + char *tmp; + my_ulonglong locIn = dbCount(db, "Presence", "RegionID != '00000000-0000-0000-0000-000000000000'"); // Locals online but not HGing, and HGers in world. + my_ulonglong HGin = dbCount(db, "Presence", "UserID NOT IN (SELECT PrincipalID FROM UserAccounts)"); // HGers in world. + + // Collect stats about members. + replaceLong(stats->stats, "hgers", HGin); + replaceLong(stats->stats, "inworld", locIn - HGin); + tmp = xmprintf("GridExternalName != '%s'", stats->stats->getstr(stats->stats, "uri", false)); + replaceLong(stats->stats, "outworld", dbCount(db, "hg_traveling_data", tmp)); + free(tmp); + replaceLong(stats->stats, "members", dbCount(db, "UserAccounts", NULL)); + + // Count local and HG visitors for the last 30 and 60 days. + locIn = dbCountJoin(db, "GridUser", "GridUser.UserID", "INNER JOIN UserAccounts ON GridUser.UserID = UserAccounts.PrincipalID", + "Login > UNIX_TIMESTAMP(FROM_UNIXTIME(UNIX_TIMESTAMP(now()) - 2419200))"); + HGin = dbCount(db, "GridUser", "Login > UNIX_TIMESTAMP(FROM_UNIXTIME(UNIX_TIMESTAMP(now()) - 2419200))"); + replaceLong(stats->stats, "locDay30", locIn); + replaceLong(stats->stats, "day30", HGin); + replaceLong(stats->stats, "HGday30", HGin - locIn); + + locIn = dbCountJoin(db, "GridUser", "GridUser.UserID", "INNER JOIN UserAccounts ON GridUser.UserID = UserAccounts.PrincipalID", + "Login > UNIX_TIMESTAMP(FROM_UNIXTIME(UNIX_TIMESTAMP(now()) - 4838400))"); + HGin = dbCount(db, "GridUser", "Login > UNIX_TIMESTAMP(FROM_UNIXTIME(UNIX_TIMESTAMP(now()) - 4838400))"); + replaceLong(stats->stats, "locDay60", locIn); + replaceLong(stats->stats, "day60", HGin); + replaceLong(stats->stats, "HGday60", HGin - locIn); + + // Collect stats about sims. + replaceLong(stats->stats, "sims", dbCount(db, "regions", NULL)); + replaceLong(stats->stats, "onlineSims", dbCount(db, "regions", "sizeX != 0")); + replaceLong(stats->stats, "varRegions", dbCount(db, "regions", "sizeX > 256 or sizeY > 256")); + replaceLong(stats->stats, "singleSims", dbCount(db, "regions", "sizeX = 256 and sizeY = 256")); + replaceLong(stats->stats, "offlineSims", dbCount(db, "regions", "sizeX = 0")); + + // Calculate total size of all regions. + my_ulonglong simSize = 0; + static dbRequest *rgnSizes = NULL; + if (NULL == rgnSizes) + { + static char *szi[] = {NULL}; + static char *szo[] = {"sizeX", "sizeY", NULL}; + rgnSizes = xzalloc(sizeof(dbRequest)); + rgnSizes->db = db; + rgnSizes->table = "regions"; + rgnSizes->inParams = szi; + rgnSizes->outParams = szo; + rgnSizes->where = "sizeX != 0"; + } + dbDoSomething(rgnSizes, FALSE); + rowData *rows = rgnSizes->rows; + qlist_obj_t obj; + memset((void*)&obj, 0, sizeof(obj)); // must be cleared before call + rows->rows->lock(rows->rows); + while (rows->rows->getnext(rows->rows, &obj, false) == true) + { + qhashtbl_t *row = (qhashtbl_t *) obj.data; + my_ulonglong x = 0, y = 0; + + tmp = row->getstr(row, "sizeX", false); + if (NULL == tmp) + E("No regions.sizeX!"); + else + x = atoll(tmp); + tmp = row->getstr(row, "sizeY", false); + if (NULL == tmp) + E("No regions.sizeY!"); + else + y = atoll(tmp); + simSize += x * y; + free(row); + } + rows->rows->unlock(rows->rows); + free(rows->rows); + free(rows->fieldNames); + free(rows); + + tmp = xmprintf("%lu", simSize); + stats->stats->putstr(stats->stats, "simsSize", tmp); + free(tmp); + gettimeofday(&(stats->last), NULL); + } + + return stats; +} + + +qhashtbl_t *toknize(char *text, char *delims) +{ + qhashtbl_t *ret = qhashtbl(0, 0); + + if (NULL == text) + return ret; + + char *txt = xstrdup(text), *token, dlm = ' ', *key, *val = NULL; + int offset = 0; + + while((token = qstrtok(txt, delims, &dlm, &offset)) != NULL) + { + if (delims[0] == dlm) + { + key = token; + val = &txt[offset]; + } + else if (delims[1] == dlm) + { + ret->putstr(ret, qstrtrim_head(key), token); +D(" %s = %s", qstrtrim_head(key), val); + val = NULL; + } + } + if (NULL != val) +{ + ret->putstr(ret, qstrtrim_head(key), val); +D(" %s = %s", qstrtrim_head(key), val); +} + free(txt); + return ret; +} + +void santize(qhashtbl_t *tbl, bool decode) +{ + qhashtbl_obj_t obj; + + memset((void*)&obj, 0, sizeof(obj)); + tbl->lock(tbl); + while(tbl->getnext(tbl, &obj, true) == true) + { + char *n = obj.name, *o = (char *) obj.data; + + if (decode) + qurl_decode(o); + +// if ((strcmp(n, "password") != 0) && (strcmp(n, "psswd") != 0)) + { + // Poor mans Bobby Tables protection. + o = qstrreplace("tr", o, "'", "_"); + o = qstrreplace("tr", o, "\"", "_"); + o = qstrreplace("tr", o, ";", "_"); + o = qstrreplace("tr", o, "(", "_"); + o = qstrreplace("tr", o, ")", "_"); + } + + tbl->putstr(tbl, n, o); + free(o); + } + tbl->unlock(tbl); +} + +void outize(qgrow_t *reply, qhashtbl_t *tbl, char *label) +{ + reply->addstrf(reply, "%s:
\n
\n", label);
+  qhashtbl_obj_t obj;
+  memset((void*)&obj, 0, sizeof(obj));
+  tbl->lock(tbl);
+  while(tbl->getnext(tbl, &obj, false) == true)
+    reply->addstrf(reply, "   %s = %s\n", obj.name, (char *) obj.data);
+  tbl->unlock(tbl);
+  reply->addstr(reply, "
\n"); +} + + +// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie +enum cookieSame +{ + CS_NOT, + CS_STRICT, + CS_LAX, // Apparently the default set by browsers these days. + CS_NONE +}; +typedef struct _cookie cookie; +struct _cookie +{ + char *cookie, *value, *domain, *path; + // char *expires; // Use maxAge instead, it's far simpler to figure out. + int maxAge; + boolean secure, httpOnly; + enum cookieSame site; +}; + +cookie *setCookie(reqData *Rd, char *cki, char *value) +{ + cookie *ret = xzalloc(sizeof(cookie)); + int l, i; + + ret->cookie = xstrdup(cki); + // Validate this, as there is a limited set of characters allowed. + qstrreplace("tr", ret->cookie, "()<>@,;:\\\"/[]?={} \t", "_"); + l = strlen(ret->cookie); + for (i = 0; i < l; i++) + { + if (iscntrl(ret->cookie[i]) != 0) + ret->cookie[i] = '_'; + } + ret->value = qurl_encode(value, strlen(value)); + ret->httpOnly = TRUE; + ret->site = CS_STRICT; + ret->secure = TRUE; + ret->path = xstrdup(getStrH(Rd->headers, "SCRIPT_NAME")); + Rd->Rcookies->put(Rd->Rcookies, cki, ret, sizeof(*ret)); + + return ret; +} + +char *getCookie(qhashtbl_t *cookies, char *cki) +{ + char *ret = NULL; + cookie *ck = (cookie *) cookies->get(cookies, cki, NULL, false); + + if (NULL != ck) + ret = ck->value; + + return ret; +} + +void outizeCookie(qgrow_t *reply, qhashtbl_t *tbl, char *label) +{ + reply->addstrf(reply, "%s:
\n
\n", label);
+  qhashtbl_obj_t obj;
+  memset((void*)&obj, 0, sizeof(obj));
+  tbl->lock(tbl);
+  while(tbl->getnext(tbl, &obj, false) == true)
+    reply->addstrf(reply, "   %s = %s\n", obj.name, ((cookie *) obj.data)->value);
+  tbl->unlock(tbl);
+  reply->addstr(reply, "
\n"); +} + + + +enum fragmentType +{ + FT_TEXT, + FT_PARAM, + FT_LUA +}; + +typedef struct _fragment fragment; +struct _fragment +{ + enum fragmentType type; + int length; + char *text; +}; + +static void HTMLheader(qgrow_t *reply, char *title) +{ + reply->addstrf(reply, + "\n" + " \n" + " %s\n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " " + , title); +} + +static void HTMLdebug(qgrow_t *reply) +{ + reply->addstrf(reply, + "
\n" + "

\n" + "

\n" + "

DEBUG

\n" + "
\n" + "

DEBUG log

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

\n" + "
\n" + ); +} + +static void HTMLtable(qgrow_t *reply, MYSQL *db, MYSQL_RES *result, char *caption, char *URL, char *id) +{ + char *tbl = ""; + char *address, *addrend, *t, *t0; + int count = 0, c = -1, i; + MYSQL_ROW row; + MYSQL_FIELD *fields = mysql_fetch_fields(result); + + reply->addstrf(reply, "\n", 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, "", s); + } + reply->addstr(reply, "\n"); + } + + if (NULL != URL) + { + free(address); + address = xmprintf("addstrf(reply, "", address, id, t0, t0, addrend); + else + reply->addstrf(reply, "", t0); + } + } + reply->addstr(reply, "\n"); + + free(address); + count++; + } + + reply->addstr(reply, "
%s
%s
%s&%s=%s\">%s%s%s
"); + mysql_free_result(result); +} + +static void HTMLhidden(qgrow_t *reply, char *name, char *val) +{ + reply->addstrf(reply, " \n", name, val); +} + +static void HTMLform(qgrow_t *reply, char *action, char *token) +{ + reply->addstrf(reply, "
\n", action); + if ((NULL != token) && ('\0' != token[0])) + HTMLhidden(reply, "munchie", token); +} +static void HTMLformEnd(qgrow_t *reply) +{ + reply->addstr(reply, "
\n"); +} + +static void HTMLcheckBox(qgrow_t *reply, char *name, char *title, boolean checked) +{ + if (checked) + reply->addstrf(reply, "

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

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

%s : addstrf(reply, "value=\"%s\"", val); + if (0 < size) + reply->addstrf(reply, " size=\"%d\"", size); + if (0 < max) + reply->addstrf(reply, " maxlength=\"%d\"", max); + if (required) + reply->addstr(reply, " required"); + reply->addstr(reply, ">

\n"); +} + +static void HTMLselect(qgrow_t *reply, char *title, char *name) +{ + if (NULL == title) + reply->addstrf(reply, " \n", title, name); +} +static void HTMLselectEnd(qgrow_t *reply) +{ + reply->addstr(reply, " \n

\n"); +} +static void HTMLselectEndNo(qgrow_t *reply) +{ + reply->addstr(reply, " "); +} + +static void HTMLoption(qgrow_t *reply, char *title, boolean selected) +{ + char *sel = ""; + + if (selected) + sel = " selected"; + reply->addstrf(reply, " \n", title, sel, title); +} + +static void HTMLbutton(qgrow_t *reply, char *title) +{ + reply->addstrf(reply, " \n", title, title); +} + +static void HTMLlist(qgrow_t *reply, char *title, qlist_t *list) +{ + qlist_obj_t obj; + + reply->addstrf(reply, "
    %s\n", title); + memset((void*)&obj, 0, sizeof(obj)); // must be cleared before call + list->lock(list); + while (list->getnext(list, &obj, false) == true) + reply->addstrf(reply, "
  • %s
  • \n", (char *) obj.data); + list->unlock(list); + 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) + { + Rd->reply->addstrf(Rd->reply, "

FastCGI SledjChisl

\n" + "

Request number %d, Process ID: %d

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

libfcgi version: %s

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

Lua version: %s

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

LuaJIT version: %s

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

MySQL client version: %s

\n", mysql_get_client_info()); + outize(Rd->reply, Rd->headers, "Environment"); + outize(Rd->reply, Rd->cookies, "Cookies"); + outize(Rd->reply, Rd->queries, "Query"); + outize(Rd->reply, Rd->body, "POST body"); + outize(Rd->reply, Rd->stuff, "Stuff"); + showSesh(Rd->reply, &Rd->shs); + if (Rd->lnk) showSesh(Rd->reply, Rd->lnk); + outize(Rd->reply, Rd->database, "Database"); + outizeCookie(Rd->reply, Rd->Rcookies, "Reply Cookies"); + outize(Rd->reply, Rd->Rheaders, "Reply HEADERS"); + } + else if (strcmp("URL", text) == 0) + Rd->reply->addstrf(Rd->reply, "%s://%s%s", Rd->Scheme, Rd->Host, Rd->Script); + else + { + if ((tmp = Rd->stats->stats->getstr(Rd->stats->stats, text, false)) != NULL) + Rd->reply->addstr(Rd->reply, tmp); + else + Rd->reply->addstrf(Rd->reply, "%s", text); + } + break; + } + + case FT_LUA: + break; + } +} + +static void HTMLfooter(qgrow_t *reply) +{ + reply->addstr(reply, + "
" + " \n\n"); +} + + +fragment *newFragment(enum fragmentType type, char *text, int len) +{ + fragment *frg = xmalloc(sizeof(fragment)); + frg->type = type; + frg->length = len; + frg->text = xmalloc(len + 1); + memcpy(frg->text, text, len); + frg->text[len] = '\0'; + return frg; +} + +qlist_t *fragize(char *mm, size_t length) +{ + qlist_t *fragments = qlist(QLIST_THREADSAFE); + fragment *frg0, *frg1; + + char *h; + int i, j = 0, k = 0, l, m; + + // Scan for server side includes style markings. + for (i = 0; i < length; i++) + { + if (i + 5 < length) + { + if (('<' == mm[i]) && ('!' == mm[i + 1]) && ('-' == mm[i + 2]) && ('-' == mm[i + 3]) && ('#' == mm[i + 4])) // '' + i += 4; + } + frg0 = newFragment(FT_TEXT, &mm[k], m - k); + fragments->addlast(fragments, frg0, sizeof(*frg0)); + fragments->addlast(fragments, frg1, sizeof(*frg1)); + k = i; + break; + } + } + } + } + } + } + } + } + } + frg0 = newFragment(FT_TEXT, &mm[k], length - k); + fragments->addlast(fragments, frg0, sizeof(*frg0)); + + return fragments; +} + +void unfragize(qlist_t *fragments, reqData *Rd) +{ + qlist_obj_t lobj; + memset((void *) &lobj, 0, sizeof(lobj)); + fragments->lock(fragments); + while (fragments->getnext(fragments, &lobj, false) == true) + { + fragment *frg = (fragment *) lobj.data; + if (NULL == frg->text) + { + E("NULL fragment!"); + continue; + } + HTMLfill(Rd, frg->type, frg->text, frg->length); + } + fragments->unlock(fragments); +} + +HTMLfile *checkHTMLcache(char *file) +{ + if (NULL == HTMLfileCache) + HTMLfileCache = qhashtbl(0, 0); + + HTMLfile *ret = (HTMLfile *) HTMLfileCache->get(HTMLfileCache, file, NULL, false); + int fd = open(file, O_RDONLY); + size_t length = 0; + + if (-1 == fd) + { + HTMLfileCache->remove(HTMLfileCache, file); + free(ret); + ret = NULL; + } + else + { + struct stat sb; + if (fstat(fd, &sb) == -1) + { + HTMLfileCache->remove(HTMLfileCache, file); + free(ret); + ret = NULL; + E("Failed to stat %s", file); + } + else + { + if ((NULL != ret) && (ret->last.tv_sec < sb.st_mtim.tv_sec)) + { + HTMLfileCache->remove(HTMLfileCache, file); + free(ret); + ret = NULL; + } + + if (NULL == ret) + { + char *mm = MAP_FAILED; + + ret = xmalloc(sizeof(HTMLfile)); + length = sb.st_size; + ret->last.tv_sec = sb.st_mtim.tv_sec; + ret->last.tv_nsec = sb.st_mtim.tv_nsec; + + I("Loading web template %s", file); + D("Web template %s is %d bytes long.", file, length); + + mm = mmap(NULL, length, PROT_READ, MAP_SHARED | MAP_POPULATE, fd, 0); + if (mm == MAP_FAILED) + { + HTMLfileCache->remove(HTMLfileCache, file); + free(ret); + ret = NULL; + E("Failed to mmap %s", file); + } + else + { + ret->fragments = fragize(mm, length); + if (-1 == munmap(mm, length)) + FCGI_fprintf(FCGI_stderr, "Failed to munmap %s\n", file); + + HTMLfileCache->put(HTMLfileCache, file, ret, sizeof(*ret)); + } + } + close(fd); + } + } + + return ret; +} + + +/* TODO - + + On new user / password reset. +. Both should have all the same security concerns as the login page, they are basically logins. +. Codes should be "very long", "(for example, 16 case-sensitive alphanumeric characters)" +. "confirm" button hit on "accountCreationPage" or "resetPasswordPage" +. generate a new token, keep it around for idleTimeOut (or at least 24 hours), call it .linky instead of .lua +. hash the linky for the file name, for the same reason we hash the hashish with pepper for the leaf-node. +. Include user level field, new users get -200. +. Store the linky itself around somewhere we can find it quickly for logged in users. +. store it in the regenerated session +. Scratch that, we should never store the raw linky, see above about hashing the linky. + +. The linky is just like the session token, create it in exactly the same way. +. Linky is base64() of the binary, so it's short enough to be a file name, and not too long for the URL. +. But we only get to send one of them as a linky URL, no backup cookies / body / headers. +. Sooo, need to separate the session stuff out of Rd->stuff. +. Use two separate qhashtbl's, Rd->session and Rd->linky. + + For new user +. create their /opt/opensim_SC/var/lib/users/UUID.lua account record, and symlink firstName_lastName.lua to it. +. They can log on, + but all they can do is edit their email to send a new validation code, and enter the validation code. + They can reset their password. +. Warn them on login and any page refresh that there is an outstanding validation awaiting them. + For reset password +. Let them do things as normal, in case this was just someone being mean to them, coz their email addy might be public. +. Including the usual logging out and in again with their old password. +. Warn them on login and any page refresh that there is an outstanding password reset awaiting them. + email linky, which is some or all of the token result bits strung together, BASE64 encode the result. +. regenerate the usual token +. user clicks on the linky (or just enters the linky in a field) +. validate the linky token. + compare the level field to the linky type in the linky URL, new users -200 would be "../validateUser/.." and not "../resetPassword/.." +. delete the linky token +. Particularly important for the forgotten password email, since now that token is in the wild, and is used to reset a password. + Which begs the question, other than being able to recieve the email, how do we tell it's them? + Security questions suck, too easily guessed. + Ask their DoB. Still sucky, coz "hey it's my birthday today" is way too leaky. + This is what Multi Factor Autentication is good for, and that's on the TODO list. + Also, admins should have a harder time doing password resets. + Must be approved by another admin? + Must log onto the server via other means to twiddle something there? + For password reset page + Ask their DoB to help confirm it's them. + validate the DoB, delete tokens and back to the login page if they get it wrong + Should warn people on the accountCreationPage that DoB might be used this way. + ask them for the new password, twice + Create a new passwordSalt and passwordHash, store them in the auth table. + For validate new user page +. tell them they have validated + create their OpenSim account UserAccounts.UserTitle and auth tables, not GridUser table + create their GridUser record. + update their UserAccounts.Userlevel and UserAccounts.UserTitle +. send them to the login page. +. regenerate the usual token +? let user stay logged on? + Check best practices for this. + + Check password strength. + https://stackoverflow.com/questions/549/the-definitive-guide-to-form-based-website-authentication?rq=1 + Has some pointers to resources in the top answers "PART V: Checking Password Strength" section. + + "PART VI: Much More - Or: Preventing Rapid-Fire Login Attempts" and "PART VII: Distributed Brute Force Attacks" is also good for - + Login attempt throttling. + Deal with dictionary attacks by slowing down access on password failures etc. + + Deal with editing yourself. + Deal with editing others, but only as god. + + + Regularly delete old session files and ancient newbies. + + + Salts should be "lengthy" (128 bytes suggested in 2007) and random. Should be per user to. Or use a per user and a global one, and store the global one very securely. + And store hash(salt+password). + On the other hand, the OpenSim / SL password hashing function may be set in concrete in all the viewers. I'll have to find out. + So far I have discovered - + On login server side if the password doesn't start with $1$, then password = "$1$" + Util.Md5Hash(passwd); + remove the "$1$ bit + string hashed = Util.Md5Hash(password + ":" + data.Data["passwordSalt"].ToString()); + if (data.Data["passwordHash"].ToString() == hashed) + passwordHash is char(32), and as implied above, doesn't include the $1$ bit. passwordSalt is also char(32) + Cool VL and Impy at least sends the $1$ md5 hashed version over the wire. + Modern viewers obfuscate the details buried deep in C++ OOP crap. + Sent via XMLRPC + MD5 is considered broken since 2013, possibly longer. + Otherwise use a slow hashing function. bcrypt? scrypt? Argon2? PBKDF2? + https://security.stackexchange.com/questions/211/how-to-securely-hash-passwords + Should include a code in the result that tells us which algorithm was used, so we can change the algorithm at a later date. /etc/passwd does this. + Which is what the $1$ bit currently used between server and client is sorta for. + ++ Would be good to have one more level of this Rd->database stuff, so we know the type of the thing. + While qhashtbl has API for putting strings, ints, and memory, it has none for finding out what type a stored thing is. + Once I have a structure, I add things like "level needed to edit it", "OpenSim db structure to Lua file mapping", and other fanciness. + Would also help with the timestamp being stored for the session, it prints out binary gunk in the DEBUG
. + Starting to get into object oriented territory here. B-) + I'll have to do it eventually anyway. + object->tostring(object), and replace the big switch() statements in the existing db code with small functions. + That's why the qlibc stuff has that format, coz C doesn't understand the concept of passing "this" as the first argument. + https://stackoverflow.com/questions/351733/how-would-one-write-object-oriented-code-in-c + https://stackoverflow.com/questions/415452/object-orientation-in-c + http://ooc-coding.sourceforge.net/ + + +https://owasp.org/www-project-cheat-sheets/cheatsheets/Input_Validation_Cheat_Sheet.html#Email_Address_Validation +https://cheatsheetseries.owasp.org/ +https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html +https://owasp.org/www-project-cheat-sheets/cheatsheets/Authentication_Cheat_Sheet.html +https://softwareengineering.stackexchange.com/questions/46716/what-technical-details-should-a-programmer-of-a-web-application-consider-before +https://wiki.owasp.org/index.php/OWASP_Guide_Project +https://stackoverflow.com/questions/549/the-definitive-guide-to-form-based-website-authentication?rq=1 +*/ + + + +// Forward declare this here so we can use it in validation functions. +void loginPage(reqData *Rd, char *message); + +/* Four choices for the token - (https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html) + https://en.wikipedia.org/wiki/Cross-site_request_forgery + Has some more info. + +Large random value generated by a secure method (getrandom(2)). + Keep it secret, put it in hidden fields, or custom HTTP header (requires JavaScript but more secure than hidden fields). + NOT cookies or GET. Don't log it. +Cryptographically sign a session ID and timestamp. + Timestamp is for session timeouts. + Keep it secret, put it in hidden fields, or custom HTTP header (requires JavaScript but more secure than hidden fields). + Needs a secret key server side. +A strong HMAC (SHA-256 or better) of a session ID and timestamp. + The above document seems to imply that a key is used for this, the openssl EVP functions don't mention any way of supplying this key. + https://en.wikipedia.org/wiki/HMAC says there is a key as well. + https://www.openssl.org/docs/man1.1.0/man3/HMAC.html HAH! They can have keys. OpenSSL's docs suck. + Token = HMAC(sessionID+timestamp)+timestamp (Yes, timestamp is used twice). + Keep it secret, put it in hidden fields, or custom HTTP header (requires JavaScript but more secure than hidden fields). + Needs a secret key server side. +Double cookie + Large random value generated by a secure method set as a cookie and hidden field. Check they match. + Optional - encrypt / salted hash it in another cookie / hidden field. ++ Also a resin (BASE64 session key in the query string). + Not such a good idea to have something in the query, coz that screws with bookmarks. + https://security.stackexchange.com/questions/59470/double-submit-cookies-vulnerabilities + Though so far all the pages I find saying this don't say flat out say "use headers instead", though they do say "use HSTS". + https://security.stackexchange.com/questions/220797/is-the-double-submit-cookie-pattern-still-effective ++ Includes a work around that I might already be doing. + +SOOOOO - use double cookie + hidden field. + No headers, coz I need JavaScript to do that. + No hidden field when redirecting post POST to GET, coz GOT doesn't get those. + pepper = long pass phrase or some such stored in .sledjChisl.conf.lua, which has to be protected dvs1/opensimsc/0640 as well as the database credentials. + salt = large random value generated by a secure method (getrandom(2)). + seshID = large random value generated by a secure method (getrandom(2)). + timeStamp = mtime of the leaf-node file, set to current time when we are creating the token. + sesh = seshID + timeStamp + munchie = HMAC(sesh) + timeStamp The token hidden field + toke_n_munchie = HMAC(UUID + munchie) The token cookie + hashish = HMACkey(toke_n_munchie, salt) Salted token cookie & linky query +? resin = BASE64(hashish) Base64 token cookie + leaf-node = HMACkey(hashish, pepper) Stored token file name + + Leaf-node.lua (mtime is timeStamp) + IP, UUID, salt, seshID, user name, passwordSalt, passwordHash (last two for OpenSim password protocol) + + The test - (validateSesh() below) + we get hashish and toke_n_munchie + HMACkey(hashish + pepper) -> leaf-node + read leaf-node.lua -> IP, UUID, salt, seshID + get it's mtime -> timeStamp + seshID + timeStamp -> sesh + HMAC(sesh) + timeStamp -> munchie + if we got munchie in the hidden field, compare it + toke_n_munchie == HMAC(UUID + munchie) + For linky it'll be - + HMAC(UUID + munchie) -> toke_n_munchie + hashish == HMACkey(toke_n_munchie + salt) ++ If it's too old according to mtime, delete it and logout. + +I should make it easy to change the HMAC() function. Less important for these short lived sessions, more important for the linky URLs, most important for stared password hashes. + Same for the pepper. + +The required JavaScript might be like https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#xmlhttprequest--native-javascript- + NOTE - they somehow fucked up that anchor tag. + +NOTE - storing a pepper on the same RAID array as everything else will be a problem when it comes time to replace one of the disks. + It might have failed badly enough that you can't wipe it, but an attacker can dumpster dive it, replace the broken bit (firmware board), and might get lucky. + Also is a problem with SSD and rust storing good data on bad sectors in the spare sector pool, wear levelling, etc. + +https://stackoverflow.com/questions/16891729/best-practices-salting-peppering-passwords +*/ + +static void freeSesh(reqData *Rd, boolean linky, boolean wipe) +{ + char *file = NULL; + sesh *shs = &Rd->shs; + + if (linky) + { + shs = Rd->lnk; + file = xmprintf("%s/caches/sessions/%s.linky", getStrH(Rd->configs, "scRoot"), shs->leaf); + } + else + file = xmprintf("%s/caches/sessions/%s.lua", getStrH(Rd->configs, "scRoot"), shs->leaf); + + if (wipe) + I("Wiping session %s.", file); + else + I("Deleting session %s.", file); + + if ('\0' != shs->leaf[0]) + { + if (unlink(file)) + perror_msg("Unable to delete %s", file); + } + + Rd->body-> remove(Rd->body, "munchie"); + + Rd->cookies->remove(Rd->cookies, "toke_n_munchie"); + Rd->cookies->remove(Rd->cookies, "hashish"); + + cookie *ck = setCookie(Rd, "toke_n_munchie", ""); + cookie *ckh = setCookie(Rd, "hashish", ""); + ck->maxAge = -1; // Should expire immediately. + ckh->maxAge = -1; // Should expire immediately. + + qhashtbl_obj_t obj; + memset((void*)&obj, 0, sizeof(obj)); + Rd->database->lock(Rd->database); + while(Rd->database->getnext(Rd->database, &obj, false) == true) + Rd->database->remove(Rd->database, obj.name); + Rd->database->unlock(Rd->database); + + if (wipe) + { + Rd->stuff->remove(Rd->stuff, "UUID"); + Rd->stuff->remove(Rd->stuff, "name"); + Rd->stuff->remove(Rd->stuff, "level"); + Rd->stuff->remove(Rd->stuff, "passwordSalt"); + Rd->stuff->remove(Rd->stuff, "passwordHash"); + } + + if (shs->isLinky) + { + free(Rd->lnk); + Rd->lnk = NULL; + } + else + shs->leaf[0] = '\0'; +} + +static void setToken_n_munchie(reqData *Rd, boolean linky) +{ + sesh *shs = &Rd->shs; + char *file, *link = ""; + + if (linky) + { + shs = Rd->lnk; + file = xmprintf("%s/caches/sessions/%s.linky", getStrH(Rd->configs, "scRoot"), shs->leaf); + } + else + { + file = xmprintf("%s/caches/sessions/%s.lua", getStrH(Rd->configs, "scRoot"), shs->leaf); + if (NULL != Rd->lnk) + link = Rd->lnk->hashish; + } + + struct stat st; + int s = stat(file, &st); + + if (!linky) + { + cookie *ck = setCookie(Rd, "toke_n_munchie", shs->toke_n_munchie); + cookie *ckh = setCookie(Rd, "hashish", shs->hashish); + } + char *tnm = xmprintf( "toke_n_munchie = \n" + "{\n" + " ['IP']='%s',\n" + " ['name']='%s',\n" + " ['level']='%s',\n" + " ['passwordSalt']='%s',\n" + " ['passwordHash']='%s',\n" + " ['salt']='%s',\n" + " ['seshID']='%s',\n" + " ['UUID']='%s',\n" + " ['linky-hashish']='%s',\n" + "}\n" + "return toke_n_munchie\n", + getStrH(Rd->headers, "REMOTE_ADDR"), + getStrH(Rd->stuff, "name"), + getStrH(Rd->stuff, "level"), + getStrH(Rd->stuff, "passwordSalt"), + getStrH(Rd->stuff, "passwordHash"), + shs->salt, + shs->seshID, + getStrH(Rd->stuff, "UUID"), + link + ); + int fd = notstdio(xcreate_stdio(file, O_CREAT | O_WRONLY | O_TRUNC | O_CLOEXEC, S_IRUSR | S_IWUSR)); + size_t l = strlen(tnm); + + if (s) + I("Creating session %s.", file); + else + C("Updating session %s.", file); // I don't think updates can occur now. + if (l != writeall(fd, tnm, l)) + { + perror_msg("Writing %s", file); + freeSesh(Rd, linky, TRUE); + } + // Set the mtime on the file. + futimens(fd, shs->timeStamp); + xclose(fd); + free(file); +} + +static void createUser(reqData *Rd) +{ + char *file = xmprintf("%s/var/lib/users/%s.lua", getStrH(Rd->configs, "scRoot"), getStrH(Rd->stuff, "UUID")); + char *tnm = xmprintf( "user = \n" + "{\n" + " ['name']='%s',\n" +// TODO - putting these in Lua as numbers causes lua_tolstring to barf when we read them. Though Lua is supposed to convert between numbers and strings. + " ['created']='%ld',\n" + " ['email']='%s',\n" + " ['title']='%s',\n" + " ['level']='%d',\n" + " ['flags']='%d',\n" + " ['active']='%d',\n" + " ['passwordSalt']='%s',\n" + " ['passwordHash']='%s',\n" + " ['UUID']='%s',\n" + " ['DoB']='%s-%s',\n" + " ['agree']='%s',\n" + " ['adult']='%s',\n" + " ['vouched']='%s',\n" + "}\n" + "return user\n", + getStrH(Rd->stuff, "name"), + (long) Rd->shs.timeStamp[1].tv_sec, + getStrH(Rd->body, "email"), + "newbie", + -200, + 64, + 0, + getStrH(Rd->stuff, "passwordSalt"), + getStrH(Rd->stuff, "passwordHash"), + getStrH(Rd->stuff, "UUID"), + getStrH(Rd->body, "year"), + getStrH(Rd->body, "month"), + getStrH(Rd->body, "agree"), + getStrH(Rd->body, "adult"), + "off" + ); + + struct stat st; + int s = stat(file, &st); + + int fd = notstdio(xcreate_stdio(file, O_CREAT | O_WRONLY | O_TRUNC | O_CLOEXEC, S_IRUSR | S_IWUSR)); + size_t l = strlen(tnm); + + if (s) + I("Creating user %s.", file); + else + C("Updating user %s.", file); + if (l != writeall(fd, tnm, l)) + perror_msg("Writing %s", file); + else + { + char *name = Rd->stuff->getstr(Rd->stuff, "name", true); + char *nm = xmprintf("%s/var/lib/users/%s.lua", getStrH(Rd->configs, "scRoot"), qstrreplace("tr", name, " ", "_")); + + free(file); + file = xmprintf("%s.lua", getStrH(Rd->stuff, "UUID")); + I("Symlinking %s to %s", file, nm); + if (0 != symlink(file, nm)) + perror_msg("Symlinking %s to %s", file, nm); + free(nm); free(name); + } + xclose(fd); + free(file); +} + + +static void bitch(reqData *Rd, char *message, char *log) +{ + addStrL(Rd->errors, message); + E("%s %s %s %s %s", getStrH(Rd->headers, "REMOTE_ADDR"), getStrH(Rd->stuff, "UUID"), getStrH(Rd->stuff, "name"), message, log); +} + +/* "A token cookie that references a non-existent session, its value should be replaced immediately to prevent session fixation." +https://owasp.org/www-community/attacks/Session_fixation + Which describes the problem, but offers no solution. + See https://stackoverflow.com/questions/549/the-definitive-guide-to-form-based-website-authentication?rq=1. +I think this means send a new cookie. + I clear out the cookies and send blank ones with -1 maxAge, so they should get deleted. +*/ +static void bitchSession(reqData *Rd, char *message, char *log) +{ + addStrL(Rd->errors, message); + C("%s %s %s %s %s", getStrH(Rd->headers, "REMOTE_ADDR"), getStrH(Rd->stuff, "UUID"), getStrH(Rd->stuff, "name"), message, log); + Rd->vegOut = TRUE; +} +static sesh *newSesh(reqData *Rd, boolean linky) +{ + unsigned char *md5hash = xzalloc(17); + char *toke_n_munchie, *munchie, *hashish, *t0, *t1; + char uuid[37]; + uuid_t binuuid; + sesh *ret = &Rd->shs; + +W("New sesh"); + if (linky) + { + Rd->lnk = xzalloc(sizeof(sesh)); + ret = Rd->lnk; + } + + char buf[128]; // 512 bits. + int numBytes = getrandom((void *)buf, sizeof(buf), GRND_NONBLOCK); + + // NOTE that getrandom() returns random bytes, which may include '\0'. + if (-1 == numBytes) + { + perror_msg("Unable to generate a suitable random number."); + // EAGAIN - not enough entropy, try again. + // EINTR - signal handler interrupted it, try again. + } + else + { + qstrcpy(ret->salt, sizeof(ret->salt), qhex_encode(buf, sizeof(buf))); +//D("salt %s", ret->salt); + numBytes = getrandom((void *)buf, sizeof(buf), GRND_NONBLOCK); + if (-1 == numBytes) + perror_msg("Unable to generate a suitable random number."); + else + { + qstrcpy(ret->seshID, sizeof(ret->seshID), qhex_encode(buf, sizeof(buf))); +//D("seshID %s", ret->seshID); + + ret->timeStamp[0].tv_nsec = UTIME_OMIT; + ret->timeStamp[0].tv_sec = UTIME_OMIT; + if (-1 == clock_gettime(CLOCK_REALTIME, &ret->timeStamp[1])) + perror_msg("Unable to get the time."); + else + { + // tv_sec is a time_t, tv_nsec is a long, but the actual type of time_t isn't well defined, it's some sort of integer. + t0 = xmprintf("%s%ld.%ld", ret->seshID, (long) ret->timeStamp[1].tv_sec, ret->timeStamp[1].tv_nsec); + qstrcpy(ret->sesh, sizeof(ret->sesh), t0); +//D("sesh %s", ret->sesh); + t1 = myHMAC(t0, FALSE); + free(t0); + munchie = xmprintf("%s%ld.%ld", t1, (long) ret->timeStamp[1].tv_sec, ret->timeStamp[1].tv_nsec); + free(t1); + qstrcpy(ret->munchie, sizeof(ret->munchie), munchie); +//D("munchie %s", ret->munchie); + t0 = xmprintf("%s%s", getStrH(Rd->stuff, "UUID"), munchie); + toke_n_munchie = myHMAC(t0, FALSE); + free(t0); + qstrcpy(ret->toke_n_munchie, sizeof(ret->toke_n_munchie), toke_n_munchie); +//D("toke_n_munchie %s", ret->toke_n_munchie); + hashish = myHMACkey(ret->salt, toke_n_munchie, FALSE); + qstrcpy(ret->hashish, sizeof(ret->hashish), hashish); +//D("hashish %s", ret->hashish); + t0 = myHMACkey(getStrH(Rd->configs, "pepper"), hashish, TRUE); + qstrcpy(ret->leaf, sizeof(ret->leaf), t0); +//D("leaf %s", ret->leaf); + free(t0); + ret->isLinky = linky; + setToken_n_munchie(Rd, linky); + } + } + } + + free(md5hash); + return ret; +} + +char *checkLinky(reqData *Rd) +{ + char *ret = xstrdup(""), *t0 = getStrH(Rd->stuff, "linky-hashish"); + + if ('\0' != t0[0]) + { + char *t1 = qurl_encode(t0, strlen(t0)); + free(ret); + ret = xmprintf("

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

\n", + Rd->Host, Rd->RUri, t1, t0); + free(t1); + } + return ret; +} + + +boolean prevalidate(qhashtbl_t *data, char *name) +{ + if ('\0' != getStrH(data, name)[0]) + { + I("Already validated %s.", name); + return TRUE; + } + return FALSE; +} + +boolean badBoy(int ret, reqData *Rd, qhashtbl_t *data, char *name, char *value) +{ + if (NULL == value) + value = getStrH(data, name); + + if (0 != ret) + { + char *t = xmprintf("BAD - %s", name); + + Rd->stuff->putstr(Rd->stuff, t, value); + Rd->stuff->remove(data, name); + free(t); + return TRUE; + } + data->putstr(data, name, value); + return FALSE; +} + +char *months[] = +{ + "january", + "february", + "march", + "april", + "may", + "june", + "july", + "august", + "september", + "october", + "november", + "december" +}; + + + +int LuaToHash(reqData *Rd, char *file, char *var, qhashtbl_t *tnm, int ret, struct stat *st, struct timespec *now, char *type) +{ + struct timespec then; + + if (-1 == clock_gettime(CLOCK_REALTIME, &then)) + perror_msg("Unable to get the time."); + I("Reading %s file %s", type, file); + if (0 != stat(file, st)) + { + bitchSession(Rd, "No such thing.", "No file."); + perror_msg("Unable to stat %s", file); + ret++; + } + else + { + int status = luaL_loadfile(Rd->L, file), result; + + if (status) + { + bitchSession(Rd, "No such thing.", "Can't load file."); + E("Couldn't load file: %s", lua_tostring(Rd->L, -1)); + ret++; + } + else + { + result = lua_pcall(Rd->L, 0, LUA_MULTRET, 0); + + if (result) + { + bitchSession(Rd, "Broken thing.", "Can't run file."); + E("Failed to run script: %s", lua_tostring(Rd->L, -1)); + ret++; + } + else + { + lua_getglobal(Rd->L, var); + lua_pushnil(Rd->L); + + while(lua_next(Rd->L, -2) != 0) + { + char *n = (char *) lua_tostring(Rd->L, -2); + + if (lua_isstring(Rd->L, -1)) + { + tnm->putstr(tnm, n, (char *) lua_tostring(Rd->L, -1)); +//D("Reading %s %s", n, getStrH(tnm, n)); + } + else + { + char *v = (char *) lua_tostring(Rd->L, -1); + W("Unknown Lua variable type for %s = %s", n, v); + } + lua_pop(Rd->L, 1); + } + + if (-1 == clock_gettime(CLOCK_REALTIME, now)) + perror_msg("Unable to get the time."); + double n = (now->tv_sec * 1000000000.0) + now->tv_nsec; + double t = (then.tv_sec * 1000000000.0) + then.tv_nsec; + T("Reading %s file took %lf seconds", type, (n - t) / 1000000000.0); + } + } + } + + return ret; +} + +static int validateSesh(reqData *Rd, qhashtbl_t *data) +{ + int ret = 0; + boolean linky = FALSE; + + if ('\0' != Rd->shs.leaf[0]) + { + I("Already validated session."); + return ret; + } + + I("Validating session."); + + char *toke_n_munchie = "", *munchie = "", *hashish = "", + *leaf, *timeStamp = "", *seshion = "", *seshID = "", + *t0, *t1; + + // In this case the session stuff has to come from specific places. + hashish = getStrH(Rd->queries, "hashish"); +//D("O hashish %s", hashish); + if ('\0' != hashish[0]) + linky = TRUE; + else + { + toke_n_munchie = getStrH(Rd->cookies, "toke_n_munchie"); + munchie = getStrH(Rd->body, "munchie"); + hashish = getStrH(Rd->cookies, "hashish"); + if (('\0' == toke_n_munchie[0]) || (('\0' == hashish[0]))) + { + bitchSession(Rd, "Invalid session.", "No or blank hashish or toke_n_munchie."); + ret++; + } + } + +//D("O hashish %s", hashish); +//D("O toke_n_munchie %s", toke_n_munchie); +//D("O munchie %s", munchie); + if (0 == ret) + { + struct stat st; + struct timespec now; + + leaf = myHMACkey(getStrH(Rd->configs, "pepper"), hashish, TRUE); +//D("leaf %s", leaf); + if (linky) + t0 = xmprintf("%s/caches/sessions/%s.linky", getStrH(Rd->configs, "scRoot"), leaf); + else + t0 = xmprintf("%s/caches/sessions/%s.lua", getStrH(Rd->configs, "scRoot"), leaf); + + qhashtbl_t *tnm = qhashtbl(0, 0); + ret = LuaToHash(Rd, t0, "toke_n_munchie", tnm, ret, &st, &now, "session"); + free(t0); + + if (0 == ret) + { + { + { + // This is apparently controversial, I added it coz some of the various security docs suggested it's a good idea. + // https://security.stackexchange.com/questions/139952/why-arent-sessions-exclusive-to-an-ip-address?rq=1 + // Includes various reasons why it's bad. + // Another good reason why it is bad, TOR. + // So should make this a user option, like Mantis does. + if (strcmp(getStrH(Rd->headers, "REMOTE_ADDR"), getStrH(tnm, "IP")) != 0) + { + bitchSession(Rd, "Wrong IP for session.", "Session IP doesn't match."); + ret++; + } + else + { + timeStamp = xmprintf("%ld.%ld", (long) st.st_mtim.tv_sec, st.st_mtim.tv_nsec); +//D("timeStamp %s", timeStamp); + seshion = xmprintf("%s%s", tnm->getstr(tnm, "seshID", false), timeStamp); +//D("sesh %s", seshion); + t0 = myHMAC(seshion, FALSE); + munchie = xmprintf("%s%s", t0, timeStamp); +//D("munchie %s", munchie); + free(t0); + t1 = getStrH(Rd->body, "munchie"); + if ('\0' != t1[0]) + { + if (strcmp(t1, munchie) != 0) + { + bitchSession(Rd, "Wrong munchie for session.", "HMAC(seshID + timeStamp) != munchie"); + ret++; + } + else + { + t0 = xmprintf("%s%s", getStrH(tnm, "UUID"), munchie); + t1 = myHMAC(t0, FALSE); + free(t0); + +//D("toke_n_munchie %s", t1); + if (strcmp(t1, toke_n_munchie) != 0) + { + bitchSession(Rd, "Wrong toke_n_munchie for session.", "HMAC(UUID + munchie) != toke_n_munchie"); + ret++; + } + free(t1); + } + } + + if (0 == ret) + { + if (linky) + { + t0 = xmprintf("%s%s", getStrH(tnm, "UUID"), munchie); + t1 = myHMAC(t0, FALSE); + free(t0); + toke_n_munchie = t1; +//D("toke_n_munchie %s", t1); + } + t1 = myHMACkey(getStrH(tnm, "salt"), toke_n_munchie, FALSE); +//D("hashish %s", t1); + if (strcmp(t1, hashish) != 0) + { + bitchSession(Rd, "Wrong hashish for session.", "HMAC(toke_n_munchie + salt) != hashish"); + ret++; + } + + if (now.tv_sec > st.st_mtim.tv_sec + idleTimeOut) + { + W("Session idled out."); + Rd->vegOut = TRUE; + } + else + { + if (now.tv_sec > st.st_mtim.tv_sec + seshTimeOut) + { + W("Session timed out."); + Rd->vegOut = TRUE; + } + else + { +W("Validated session."); + sesh *shs = &Rd->shs; + + qstrcpy(shs->leaf, sizeof(shs->leaf), leaf); + if (linky) + { +W("Validated session linky."); + addStrL(Rd->messages, "Congratulations, you have validated your new account. Now you can log onto the web site."); + addStrL(Rd->messages, "NOTE - you wont be able to log onto the grid until your new account has been approved."); + Rd->lnk = xzalloc(sizeof(sesh)); + qstrcpy(Rd->lnk->leaf, sizeof(Rd->lnk->leaf), leaf); + Rd->chillOut = TRUE; + freeSesh(Rd, linky, FALSE); + qstrcpy(Rd->lnk->leaf, sizeof(Rd->lnk->leaf), ""); + Rd->func = (pageBuildFunction) loginPage; + Rd->doit = "logout"; +// TODO - we might want to delete their old .lua session as well. Maybe? Don't think we have any suitable codes to find it. + } + else + { + qstrcpy(shs->sesh, sizeof(shs->sesh), seshion); + qstrcpy(shs->toke_n_munchie, sizeof(shs->toke_n_munchie), toke_n_munchie); + qstrcpy(shs->hashish, sizeof(shs->hashish), hashish); + qstrcpy(shs->munchie, sizeof(shs->munchie), munchie); + qstrcpy(shs->salt, sizeof(shs->salt), tnm->getstr(tnm, "salt", false)); + qstrcpy(shs->seshID, sizeof(shs->seshID), tnm->getstr(tnm, "seshID", false)); + shs->timeStamp[0].tv_nsec = UTIME_OMIT; + shs->timeStamp[0].tv_sec = UTIME_OMIT; + memcpy(&shs->timeStamp[1], &st.st_mtim, sizeof(struct timespec)); + t0 = tnm->getstr(tnm, "linky-hashish", false); + if (NULL != t0) + Rd->stuff->putstr(Rd->stuff, "linky-hashish", t0); + } + } + Rd->stuff->putstr(Rd->stuff, "name", tnm->getstr(tnm, "name", true)); + Rd->stuff->putstr(Rd->stuff, "UUID", tnm->getstr(tnm, "UUID", true)); + Rd->stuff->putstr(Rd->stuff, "level", tnm->getstr(tnm, "level", true)); + Rd->stuff->putstr(Rd->stuff, "passwordSalt", tnm->getstr(tnm, "passwordSalt", true)); + Rd->stuff->putstr(Rd->stuff, "passwordHash", tnm->getstr(tnm, "passwordHash", true)); + Rd->database->putstr(Rd->database, "UserAccounts.PrincipalID", tnm->getstr(tnm, "UUID", true)); + } + } + + } + } + } + } + } + + return ret; +} + +static int validateDoB(reqData *Rd, qhashtbl_t *data) +{ + int ret = 0, i; + char *t; + + if (prevalidate(Rd->stuff, "year")) return ret; + if (prevalidate(Rd->stuff, "month")) return ret; + + I("Validating DoB."); + t = getStrH(data, "year"); + if ((NULL == t) || ('\0' == t[0])) + { + bitch(Rd, "Please supply a year of birth.", "None supplied."); + ret++; + } + else + { + i = atoi(t); + if ((1900 > i) || (i > 2020)) + { + bitch(Rd, "Please supply a year of birth.", "Out of range."); + ret++; + } + } + + t = getStrH(data, "month"); + if ((NULL == t) || ('\0' == t[0])) + { + bitch(Rd, "Please supply a month of birth.", "None supplied."); + ret++; + } + else + { + for (i = 0; i < 12; i++) + { + if (strcmp(months[i], t) == 0) + break; + } + if (12 == i) + { + bitch(Rd, "Please supply a month of birth.", "Out of range"); + ret++; + } + } + + badBoy(ret, Rd, data, "month", NULL); + badBoy(ret, Rd, data, "year", NULL); + return ret; +} + +static int validateEmail(reqData *Rd, qhashtbl_t *data) +{ + int ret = 0; + char *email = getStrH(data, "email"); + char *emayl = getStrH(data, "emayl"); + + if ((strcmp("create", Rd->doit) != 0) && (strcmp("update", Rd->doit) != 0)) + return ret; + + if (prevalidate(Rd->stuff, "email")) return ret; + if (prevalidate(Rd->stuff, "emayl")) return ret; + + I("Validating email."); + if ((NULL == email) || (NULL == emayl) || ('\0' == email[0]) || ('\0' == emayl[0])) + { + bitch(Rd, "Please supply an email address.", "None supplied."); + ret++; + } + else if (strcmp(email, emayl) != 0) + { + bitch(Rd, "Email addresses are not the same.", ""); + ret++; + } + else if (!qstr_is_email(email)) + { + bitch(Rd, "Please supply a proper email address.", "Failed qstr_is_email()"); + ret++; + } + else + { +// TODO - do other email checks - does the domain exist, .. + } + + badBoy(ret, Rd, data, "email", email); + badBoy(ret, Rd, data, "emayl", emayl); + return ret; +} + +static int validateLegal(reqData *Rd, qhashtbl_t *data) +{ + int ret = 0; + char *t; + + if (prevalidate(Rd->stuff, "adult")) return ret; + if (prevalidate(Rd->stuff, "agree")) return ret; + + + I("Validating legal."); + t = getStrH(data, "adult"); + if ((NULL == t) || (strcmp("on", t) != 0)) + { + bitch(Rd, "You must be an adult to enter this world.", ""); + ret++; + } + t = getStrH(data, "agree"); + if ((NULL == t) || (strcmp("on", t) != 0)) + { + bitch(Rd, "You must agree to the Terms & Conditions of Use.", ""); + ret++; + } + + badBoy(ret, Rd, data, "adult", NULL); + badBoy(ret, Rd, data, "agree", NULL); + return ret; +} + +static int validateName(reqData *Rd, qhashtbl_t *data) +{ + boolean login = strcmp("login", Rd->doit) == 0; + int ret = 0; + unsigned char *name = data->getstr(data, "name", true); // We have to be unsigned coz of isalnum(). + char *where = NULL; + + if (strcmp("logout", Rd->doit) == 0) + return ret; + + if (prevalidate(Rd->database, "UserAccounts.UserLevel")) return ret; + + I("Validating name."); + if ((NULL == name) || ('\0' == name[0])) + { + bitch(Rd, "Please supply an account name.", "None supplied."); + ret++; + } + else + { + int l0 = strlen(name), l1; + + if (0 == l0) + { + bitch(Rd, "Please supply an account name.", "Name is empty."); + ret++; + } + else + { + int i; + unsigned char *s = NULL; + + for (i = 0; i < l0; i++) + { + if (isalnum(name[i]) == 0) + { + + if ((' ' == name[i] && (NULL == s))) + { + s = &name[i]; + *s++ = '\0'; + l1 = strlen(s); + + // Apparently names are not case sensitive on login, but stored with case in the database. + // I confirmed that, can log in no matter what case you use. + // Seems to be good security for names to be cose insensitive. + // UserAccounts FirstName and LastName fields are both varchar(64) utf8_general_ci. + // The MySQL docs say that the "_ci" bit means comparisons will be case insensitive. So that should work fine. + + // SL docs say 31 characters each for first and last name. UserAccounts table is varchar(64) each. userinfo has varchar(50) for the combined name. + // The userinfo table seems to be obsolete. + // Singularity at least limits the total name to 64. + // I can't find any limitations on characters allowed, but I only ever see letters and digits used. Case is stored, but not significant. + // OpenSims "create user" console command doesn't sanitize it at all, even crashing on some names. + if (0 == l1) + { + bitch(Rd, "Account names have to be two words.", ""); + ret++; + } + if ((31 < i) || (31 < l1)) + { + bitch(Rd, "First and last names are limited to 31 letters each.", ""); + ret++; + } + } + else + { + bitch(Rd, "First and last names are limited to letters and digits.", ""); + ret++; + break; + } + } + } + + struct stat st; + struct timespec now; + qhashtbl_t *tnm = qhashtbl(0, 0); + int rt = 0; + + if (s) {s--; *s = '_'; s++;} + where = xmprintf("%s/var/lib/users/%s.lua", getStrH(Rd->configs, "scRoot"), name); + rt = LuaToHash(Rd, where, "user", tnm, ret, &st, &now, "user"); + if (s) {s--; *s = '\0'; s++;} + free(where); + + if (0 != rt) + { + bitch(Rd, "Login failed.", "Could not read user Lua file."); + ret += rt; + } + else + { + Rd->database->putstr(Rd->database, "UserAccounts.FirstName", name); + Rd->database->putstr(Rd->database, "UserAccounts.LastName", s); + Rd->database->putstr(Rd->database, "UserAccounts.Email", getStrH(tnm, "email")); + Rd->database->putstr(Rd->database, "UserAccounts.Created", getStrH(tnm, "created")); + Rd->database->putstr(Rd->database, "UserAccounts.PrincipleID", getStrH(tnm, "UUID")); + Rd->database->putstr(Rd->database, "UserAccounts.UserLevel", getStrH(tnm, "level")); + Rd->database->putstr(Rd->database, "UserAccounts.UserFlags", getStrH(tnm, "flags")); + Rd->database->putstr(Rd->database, "UserAccounts.UserTitle", getStrH(tnm, "title")); + Rd->database->putstr(Rd->database, "UserAccounts.active", getStrH(tnm, "active")); + Rd->database->putstr(Rd->database, "auth.passwordSalt", getStrH(tnm, "passwordSalt")); + Rd->database->putstr(Rd->database, "auth.passwordHash", getStrH(tnm, "passwordHash")); + + Rd->stuff->putstr(Rd->stuff, "UUID", xstrdup(getStrH(Rd->database, "UserAccounts.PrincipalID"))); + Rd->stuff->putstr(Rd->stuff, "level", xstrdup(getStrH(Rd->database, "UserAccounts.Userlevel"))); + if (s) {s--; *s = ' '; s++;} + Rd->stuff->putstr(Rd->stuff, "name", xstrdup(name)); + if (s) {s--; *s = '\0'; s++;} + } + + + static dbRequest *acnts = NULL; + if (NULL == acnts) + { + static char *szi[] = {"FirstName", "LastName", NULL}; + static char *szo[] = {NULL}; + acnts = xzalloc(sizeof(dbRequest)); + acnts->db = Rd->db; + acnts->table = "UserAccounts"; + acnts->inParams = szi; + acnts->outParams = szo; + acnts->where = "FirstName=? and LastName=?"; + } + dbDoSomething(acnts, FALSE, name, s); + rowData *rows = acnts->rows; + if (rows) + { + int i = rows->rows->size(rows->rows); + + if (login) + { +if (rt) +{ + if (i == 0) + { + bitch(Rd, "Login failed.", "No UserAccounts record with that name."); + ret++; + } + else + { + if (i != 1) + { + bitch(Rd, "Login failed.", "More than one UserAccounts record with that name."); + ret++; + } + else + { + dbPull(Rd, "UserAccounts", rows); + Rd->stuff->putstr(Rd->stuff, "UUID", xstrdup(getStrH(Rd->database, "UserAccounts.PrincipalID"))); + Rd->stuff->putstr(Rd->stuff, "level", xstrdup(getStrH(Rd->database, "UserAccounts.Userlevel"))); + if (s) {s--; *s = ' '; s++;} + Rd->stuff->putstr(Rd->stuff, "name", xstrdup(name)); + } + } +} + } + else if (strcmp("create", Rd->doit) == 0) + { + if (i != 0) + { + bitch(Rd, "Pick a different name.", "An existing UserAccounts record matched that name."); + ret++; + } + else + { +// TODO - compare first, last, and fullname against god names, complain and fail if there's a match. + { + // Generate a UUID, check it isn't already being used. + char uuid[37]; + uuid_t binuuid; + my_ulonglong users = 0; + + do // UserAccounts.PrincipalID is a unique primary index anyway, but we want the user creation process to be a little on the slow side. + { + uuid_generate_random(binuuid); + uuid_unparse_lower(binuuid, uuid); + where = xmprintf("UserAccounts.PrincipalID = '%s'", uuid); + D("Trying new UUID %s.", where); + users = dbCount(Rd->db, "UserAccounts", where); + free(where); + } while (users != 0); +// TODO - perhaps create a place holder UserAccounts record? Then we'll have to deal with deleting them later. + + Rd->stuff->putstr(Rd->stuff, "UUID", xstrdup(uuid)); + Rd->stuff->putstr(Rd->stuff, "level", xstrdup("-200")); + Rd->database->putstr(Rd->database, "UserAccounts.PrincipalID", xstrdup(uuid)); + Rd->database->putstr(Rd->database, "UserAccounts.Userlevel", xstrdup("-200")); + Rd->database->putstr(Rd->database, "UserAccounts.firstName", xstrdup(name)); + Rd->database->putstr(Rd->database, "UserAccounts.lastName", xstrdup(s)); + if (s) {s--; *s = ' '; s++;} + Rd->stuff->putstr(Rd->stuff, "name", xstrdup(name)); + } + } + } + free(rows->rows); + free(rows->fieldNames); + free(rows); + } + + if (s) {s--; *s = ' '; s++;} + } + } + + badBoy(ret, Rd, data, "name", NULL); + return ret; +} + +static int validatePassword(reqData *Rd, qhashtbl_t *data) +{ + boolean login = strcmp("login", Rd->doit) == 0; + boolean create = strcmp("create", Rd->doit) == 0; + int ret = 0; + char *password = getStrH(data, "password"); + char *psswrd = getStrH(data, "psswrd"); + char *psswrdH = getStrH(Rd->stuff, "passwordHash"); + char *psswrdS = getStrH(Rd->stuff, "passwordSalt"); + + if (prevalidate(Rd->database, "auth.passwordSalt")) return ret; + + I("Validating password."); + if (login) + { + char *UUID = getStrH(Rd->database, "UserAccounts.PrincipalID"); + static dbRequest *auth = NULL; + if (NULL == auth) + { + static char *szi[] = {"UUID", NULL}; + static char *szo[] = {"passwordSalt", "passwordHash", NULL}; + auth = xzalloc(sizeof(dbRequest)); + auth->db = Rd->db; + auth->table = "auth"; + auth->inParams = szi; + auth->outParams = szo; + auth->where = "UUID=?"; + } + dbDoSomething(auth, FALSE, UUID); + rowData *rows = auth->rows; + if (rows) + { + int i = rows->rows->size(rows->rows); + + if (i != 1) + { + bitch(Rd, "Login failed.", "Wrong number of auth records."); + ret++; + } + else + { + qhashtbl_t *me = rows->rows->popfirst(rows->rows, NULL); + unsigned char md5hash[16]; + + if (!qhashmd5((void *) password, strlen(password), md5hash)) + { + bitch(Rd, "Login failed, internal error.", "Login - qhashmd5(password) failed."); + ret++; + } + else + { + Rd->stuff->putstr(Rd->stuff, "passwordSalt", getStrH(me, "passwordSalt")); + + char *md5ascii = qhex_encode(md5hash, 16); + char *where = xmprintf("%s:%s", md5ascii, getStrH(me, "passwordSalt")); + if (!qhashmd5((void *) where, strlen(where), md5hash)) + { + bitch(Rd, "Login failed, internal error.", "Login - qhashmd5(passwordSalt) failed."); + ret++; + } + else + { + free(md5ascii); + md5ascii = qhex_encode(md5hash, 16); + if (strcmp(md5ascii, getStrH(me, "passwordHash")) != 0) + { + bitch(Rd, "Login failed.", "passwordHash doesn't match"); + ret++; + } + Rd->stuff->putstr(Rd->stuff, "passwordHash", md5ascii); + free(md5ascii); + } + free(where); + } + } + } + } + else if (create) + { + if ((NULL == password) || ('\0' == password[0])) + { + bitch(Rd, "Please supply a password.", "Password empty or missing."); + ret++; + } + else + { + // https://stackoverflow.com/questions/246930/is-there-any-difference-between-a-guid-and-a-uuid + // Has a great discussion. + // http://tools.ietf.org/html/rfc4122 + // https://en.wikipedia.org/wiki/Universally_unique_identifier + // Useful stuff about versions and variants, a quick look says OpenSim is using version 4 (random), variant 1. + // Note versions 3 and 5 are hash based, like I wanted for SledjHamr. B-) + // https://news.ycombinator.com/item?id=14523523 is a bad blog post with a really good and lengthy comments section, concerning use as database keys. + // Better off using the 16 byte / 128 bit integer version of UUIDs for database keys, but naturally OpenSim uses char(36) / 304 bit, and sometimes varchar(255). + + // Calculate passwordSalt and passwordHash. From Opensim - + // passwordSalt = Util.Md5Hash(UUID.Random().ToString()) + // passwdHash = Util.Md5Hash(Util.Md5Hash(password) + ":" + passwordSalt) + unsigned char *md5hash = xzalloc(17); + char *salt, *hash; + char uuid[37]; + uuid_t binuuid; + + uuid_generate_random(binuuid); + uuid_unparse_lower(binuuid, uuid); + if (!qhashmd5((void *) uuid, strlen(uuid), md5hash)) + { + bitch(Rd, "Internal session error.", "Create - qhashmd5(new uuid) failed."); + ret++; + } + else + { + salt = qhex_encode(md5hash, 16); + Rd->stuff->putstr(Rd->stuff, "passwordSalt", salt); + if (!qhashmd5((void *) password, strlen(password), md5hash)) + { + bitch(Rd, "Internal session error.", "Create - qhashmd5(password) failed."); + ret++; + } + else + { + salt = qhex_encode(md5hash, 16); + hash = xmprintf("%s:%s", salt, getStrH(Rd->stuff, "passwordSalt")); + if (!qhashmd5((void *) hash, strlen(hash), md5hash)) + { + bitch(Rd, "Internal session error.", "Create - qhashmd5(passwordSalt) failed."); + ret++; + } + else + { + hash = qhex_encode(md5hash, 16); + Rd->stuff->putstr(Rd->stuff, "passwordHash", hash); + Rd->chillOut = TRUE; + } + } + } + } + } + else if (strcmp("confirm", Rd->doit) == 0) + { + if ((NULL == password) || ('\0' == password[0])) + { + bitch(Rd, "Please supply a password.", "Need two passwords."); + ret++; + } + else + { + unsigned char *md5hash = xzalloc(17); + char *pswd, *hash; + char *Osalt = getStrH(Rd->stuff, "passwordSalt"), *Ohash = getStrH(Rd->stuff, "passwordHash"); + + if (!qhashmd5((void *) password, strlen(password), md5hash)) + { + bitch(Rd, "Internal session error.", "Confirm - qhashmd5(password) failed."); + ret++; + } + else + { + pswd = qhex_encode(md5hash, 16); + hash = xmprintf("%s:%s", pswd, Osalt); + + if (!qhashmd5((void *) hash, strlen(hash), md5hash)) + { + bitch(Rd, "Internal session error.", "Confirm - qhashmd5(passwordSalt) failed."); + ret++; + } + else + { + hash = qhex_encode(md5hash, 16); + if (strcmp(hash, Ohash) != 0) + { + bitch(Rd, "Passwords are not the same.", ""); + ret++; + } + } + } + } + } + +// TODO - try to fix this, then make it portable (Windows has some other function name), then spread it through the rest of the code where needed. +// And try to find code for dealing with security enclaves, encrypted memory, and such. +// NOTE - thes get giltered through what ever web server is being used, and might leak there. + // explicit_bzero() is the magic to properly wipe things, and it exists, but the damn thing manages to hide itself. + // So gotta make sure it's actually used, to avoid the compiler optimizing bzero() away. +// explicit_bzero(password, strlen(password)); +// explicit_bzero(psswrd, strlen(psswrd)); + bzero(password, strlen(password)); + bzero(psswrd, strlen(psswrd)); + if (login) + D("User logged in with %s or %s.", password, psswrd); + else + D("Account created with %s or %s.", password, psswrd); + + return ret; +} + + +static int validateUUID(reqData *Rd, qhashtbl_t *data) +{ + int ret = 0, i; + char uuid[37], *t; + rowData *rows = NULL; + static dbRequest *uuids = NULL; + if (NULL == uuids) + { + static char *szi[] = {"PrincipalID", NULL}; + static char *szo[] = {NULL}; + uuids = xzalloc(sizeof(dbRequest)); + uuids->db = Rd->db; + uuids->table = "UserAccounts"; + uuids->inParams = szi; + uuids->outParams = szo; + uuids->where = "PrincipalID=?"; + } + + I("Validating UUID."); + if (strcmp("create", Rd->doit) == 0) + { + // Generate a UUID, check it isn't already being used, and totally ignore whatever UUID is in body. + uuid_t binuuid; + + if (prevalidate(Rd->stuff, "UUID")) return ret; + + do // UserAccounts.PrincipalID is a unique primary index anyway, but we want the user creation process to be a little on the slow side. + { + uuid_generate_random(binuuid); + uuid_unparse_lower(binuuid, uuid); + + D("Trying new UUID %s.", uuid); +// TODO - check the Lua user files as well. + dbDoSomething(uuids, FALSE, uuid); + rows = uuids->rows; + i = 0; + if (rows) + i = rows->rows->size(rows->rows); + else + { + bitch(Rd, "Internal error.", "Matching UUID record found in UserAccounts."); + ret++; + break; + } + } while (i != 0); + if (0 == ret) + { + data->putstr(data, "UUID", xstrdup(uuid)); + data->putstr(data, "NEW - UUID", uuid); + } + rows = NULL; + } + else if ((strcmp("confirm", Rd->doit) == 0) || (strcmp("logout", Rd->doit) == 0)) + { + t = getStrH(data, "UUID"); + if (36 != strlen(t)) + { + bitch(Rd, "Internal error.", "UUID isn't long enough."); + ret++; + } + else + strcpy(uuid, t); + } + else + { + if ('\0' != getStrH(Rd->database, "UserAccounts.ScopeID")[0]) return ret; + + t = getStrH(data, "UUID"); + if (36 != strlen(t)) + { + bitch(Rd, "Internal error.", "UUID isn't long enough."); + ret++; + } + else + { + strcpy(uuid, t); + dbDoSomething(uuids, FALSE, uuid); + rows = uuids->rows; + if (rows) + { + if (1 != rows->rows->size(rows->rows)) + { + bitch(Rd, "Internal error.", "No matching UUID record found in UserAccounts."); + ret++; + } + } + } + } + + if (!badBoy(ret, Rd, data, "UUID", uuid)) + { + if (rows) + { + dbPull(Rd, "UserAccounts", rows); + Rd->stuff->putstr(Rd->stuff, "level", xstrdup(getStrH(Rd->database, "UserAccounts.Userlevel"))); + free(rows->rows); + free(rows->fieldNames); + free(rows); + } + else + { + Rd->database->putstr(Rd->database, "UserAccounts.PrincipalID", xstrdup(uuid)); + } + } + + return ret; +} + + + +int validateThings(reqData *Rd, char *doit, char *name, qhashtbl_t *things) +{ + int e = 0; + + W("%s start of %s validation.", doit, name); + qlisttbl_obj_t obj; + memset((void *) &obj, 0, sizeof(obj)); + fieldValidFuncs->lock(fieldValidFuncs); + while(fieldValidFuncs->getnext(fieldValidFuncs, &obj, NULL, false) == true) + { + char *t = getStrH(things, obj.name); + + if ('\0' != t[0]) + { + validFunc *vf = (validFunc *) obj.data; + +W("Validating %s", obj.name); + if (vf->func) + e += vf->func(Rd, things); + else + E("No validation function for %s", obj.name); + } + } + fieldValidFuncs->unlock(fieldValidFuncs); + return e; +} + + +void loginPage(reqData *Rd, char *message) +{ + char *name = xstrdup(getStrH(Rd->stuff, "name")); + + Rd->stuff->remove(Rd->stuff, "UUID"); + HTMLheader(Rd->reply, " account manager"); + HTMLdebug(Rd->reply); + Rd->reply->addstrf(Rd->reply, "

account manager

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

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

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

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

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

%s

\n", message); + HTMLfooter(Rd->reply); + free(name); +} + +void accountCreationPage(reqData *Rd, char *message) +{ + char *name = getStrH(Rd->stuff, "name"); + char *toke_n_munchie = getCookie(Rd->Rcookies, "toke_n_munchie"); + char *tmp = xmalloc(16), *t; + int i, d; + +// TODO - eww lots of memory leaks here. +// TODO - need to check if qlibc does it's own free() calls, and fill in the gaps for when it doesn't. + HTMLheader(Rd->reply, " account manager"); + HTMLdebug(Rd->reply); + Rd->reply->addstrf(Rd->reply, "

account manager

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

Creating account for %s

\n", name); + Rd->reply->addstr(Rd->reply, checkLinky(Rd)); + if (0 != Rd->errors->size(Rd->messages)) + HTMLlist(Rd->reply, "messages -", Rd->messages); + HTMLform(Rd->reply, "", Rd->shs.munchie); + HTMLhidden(Rd->reply, "name", name); + HTMLhidden(Rd->reply, "UUID", getStrH(Rd->stuff, "UUID")); + HTMLhidden(Rd->reply, "psswrd", getStrH(Rd->body, "password")); + HTMLtext(Rd->reply, "email", "email", "email", getStrH(Rd->stuff, "email"), 42, 254, FALSE); + HTMLtext(Rd->reply, "email", "Repeat your email, to be sure you got it correct", "emayl", getStrH(Rd->stuff, "emayl"), 42, 254, FALSE); + Rd->reply->addstr(Rd->reply, "

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

\n"); + HTMLtext(Rd->reply, "password", "Re-enter your password", "password", "", 16, 0, FALSE); + Rd->reply->addstr(Rd->reply, "

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

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

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

\n"); + Rd->reply->addstr(Rd->reply, "
"); + HTMLselect(Rd->reply, "Date of birth", "year"); + t = getStrH(Rd->stuff, "year"); + if (NULL == t) + d = -1; + else + d = atoi(t); + HTMLoption(Rd->reply, xstrdup(""), FALSE); + for (i = 1900; i <= 2020; i++) + { + boolean sel = FALSE; + + if (i == d) + sel = TRUE; + sprintf(tmp, "%d", i); + HTMLoption(Rd->reply, xstrdup(tmp), sel); + } + HTMLselectEnd(Rd->reply); + Rd->reply->addstr(Rd->reply, ""); + HTMLselect(Rd->reply, NULL, "month"); + t = getStrH(Rd->stuff, "month"); + HTMLoption(Rd->reply, xstrdup(""), FALSE); + for (i = 0; i <= 11; i++) + { + boolean sel = FALSE; + + if ((NULL != t) && (strcmp(t, months[i]) == 0)) + sel = TRUE; + HTMLoption(Rd->reply, months[i], sel); + } + HTMLselectEnd(Rd->reply); + Rd->reply->addstr(Rd->reply, "
"); + HTMLcheckBox(Rd->reply, "adult", "I'm allegedly an adult in my country.", !strcmp("on", getStrH(Rd->stuff, "adult"))); + HTMLcheckBox(Rd->reply, "agree", "I accept the Terms & Conditions of Use.", !strcmp("on", getStrH(Rd->stuff, "agree"))); + Rd->reply->addstrf(Rd->reply, ""); // Stop Enter key on text fields triggering the first submit button. + HTMLbutton(Rd->reply, "confirm"); + HTMLbutton(Rd->reply, "cancel"); + if (0 != Rd->errors->size(Rd->errors)) + HTMLlist(Rd->reply, "errors -", Rd->errors); + Rd->reply->addstrf(Rd->reply, "

%s

\n", message); + HTMLfooter(Rd->reply); +} + +void loggedOnPage(reqData *Rd, char *message) +{ + char *name = getStrH(Rd->stuff, "name"); + char *toke_n_munchie = getCookie(Rd->Rcookies, "toke_n_munchie"); + + HTMLheader(Rd->reply, " account manager"); + HTMLdebug(Rd->reply); + Rd->reply->addstrf(Rd->reply, "

account manager

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

account for %s

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

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

\n"); +// HTMLtext(Rd->reply, "title", "text", "title", getStrH(Rh->stuff, "title"), 16, 64, TRUE); + HTMLselect(Rd->reply, "type", "type"); + HTMLoption(Rd->reply, "", false); + HTMLoption(Rd->reply, "newbie", true); + HTMLoption(Rd->reply, "validated", true); + HTMLoption(Rd->reply, "vouched for", true); + HTMLoption(Rd->reply, "approved", true); + HTMLoption(Rd->reply, "disabled", false); + HTMLoption(Rd->reply, "god", false); + HTMLselectEnd(Rd->reply); + Rd->reply->addstrf(Rd->reply, ""); // Stop Enter key on text fields triggering the first submit button. + HTMLbutton(Rd->reply, "delete"); + HTMLbutton(Rd->reply, "list"); + HTMLbutton(Rd->reply, "logout"); + HTMLbutton(Rd->reply, "update"); + if (0 != Rd->errors->size(Rd->errors)) + HTMLlist(Rd->reply, "errors -", Rd->errors); + HTMLfooter(Rd->reply); +} + +void listPage(reqData *Rd, char *message) +{ +// TODO - should check if the user is a god before allowing this. + char *name = getStrH(Rd->stuff, "name"); + char *toke_n_munchie = getCookie(Rd->Rcookies, "toke_n_munchie"); + + HTMLheader(Rd->reply, " account manager"); + HTMLdebug(Rd->reply); + Rd->reply->addstrf(Rd->reply, "

account manager

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

member accounts

\n"); + Rd->reply->addstr(Rd->reply, checkLinky(Rd)); + if (0 != Rd->errors->size(Rd->messages)) + HTMLlist(Rd->reply, "messages -", Rd->messages); + HTMLtable(Rd->reply, Rd->db, + dbSelect(Rd->db, "UserAccounts", + "CONCAT(FirstName,' ',LastName) as Name,UserTitle as Title,UserLevel as Level,UserFlags as Flags,PrincipalID as UUID", + NULL, NULL, "Name"), + "member accounts", NULL, NULL); + HTMLform(Rd->reply, "", Rd->shs.munchie); + HTMLhidden(Rd->reply, "name", name); + HTMLhidden(Rd->reply, "UUID", getStrH(Rd->stuff, "UUID")); + Rd->reply->addstrf(Rd->reply, ""); // Stop Enter key on text fields triggering the first submit button. + HTMLbutton(Rd->reply, "me"); + HTMLbutton(Rd->reply, "logout"); + if (0 != Rd->errors->size(Rd->errors)) + HTMLlist(Rd->reply, "errors -", Rd->errors); + Rd->reply->addstrf(Rd->reply, "

%s

\n", message); + HTMLfooter(Rd->reply); +} + +void account_html(char *file, reqData *Rd, HTMLfile *thisFile) +{ + boolean isGET = FALSE; + int e = 0; + char *doit = getStrH(Rd->body, "doit"); + + C("Starting dynamic page %s", file); + Rd->func = NULL; + + if (NULL == fieldValidFuncs) + { + fieldValidFuncs = qlisttbl(QLISTTBL_LOOKUPFORWARD | QLISTTBL_THREADSAFE | QLISTTBL_UNIQUE); + newValidFunc("hashish", (fieldValidFunc) validateSesh); + newValidFunc("toke_n_munchie", (fieldValidFunc) validateSesh); + newValidFunc("UUID", (fieldValidFunc) validateUUID); + newValidFunc("name", (fieldValidFunc) validateName); + newValidFunc("password", (fieldValidFunc) validatePassword); + newValidFunc("psswrd", (fieldValidFunc) validatePassword); + newValidFunc("email", (fieldValidFunc) validateEmail); + newValidFunc("emayl", (fieldValidFunc) validateEmail); + newValidFunc("year", (fieldValidFunc) validateDoB); + newValidFunc("month", (fieldValidFunc) validateDoB); + newValidFunc("adult", (fieldValidFunc) validateLegal); + newValidFunc("agree", (fieldValidFunc) validateLegal); + } + if (NULL == buildPages) + { + buildPages = qhashtbl(0, 0); + newBuildPage("login", (pageBuildFunction) loggedOnPage, (pageBuildFunction) loginPage); + newBuildPage("cancel", (pageBuildFunction) loginPage, (pageBuildFunction) loginPage); + newBuildPage("logout", (pageBuildFunction) loginPage, (pageBuildFunction) loginPage); + newBuildPage("create", (pageBuildFunction) accountCreationPage, (pageBuildFunction) loginPage); + newBuildPage("confirm", (pageBuildFunction) loggedOnPage, (pageBuildFunction) accountCreationPage); + newBuildPage("me", (pageBuildFunction) loggedOnPage, (pageBuildFunction) loginPage); + newBuildPage("update", (pageBuildFunction) loggedOnPage, (pageBuildFunction) loginPage); + newBuildPage("delete", (pageBuildFunction) loginPage, (pageBuildFunction) loginPage); + newBuildPage("list", (pageBuildFunction) listPage, (pageBuildFunction) loginPage); + } + + if ('\0' == doit[0]) + doit = getStrH(Rd->cookies, "doit"); + if ('\0' == doit[0]) + doit = "logout"; + if ('\0' != doit[0]) + { + setCookie(Rd, "doit", doit); + Rd->doit = doit; + } + + e += validateThings(Rd, doit, "cookies", Rd->cookies); + e += validateThings(Rd, doit, "body", Rd->body); + e += validateThings(Rd, doit, "queries", Rd->queries); + e += validateThings(Rd, doit, "session", Rd->stuff); + + if (NULL == Rd->func) + { + buildPage *bp = buildPages->get(buildPages, doit, NULL, false); + if (bp) + { + if (e) + { + Rd->func = bp->eFunc; + E("Got page builder ERROR function for %s, coz of %d errors.", doit, e); + } + else + { + Rd->func = bp->func; + D("Got page builder function for %s.", doit); + } + } + } + if (NULL == Rd->func) + Rd->func = (pageBuildFunction) loginPage; + + if (strcmp("https", Rd->Scheme) != 0) + { + Rd->Rheaders->putstr (Rd->Rheaders, "Status", "301 Moved Permanently"); + Rd->Rheaders->putstrf(Rd->Rheaders, "Location", "https://%s%s", Rd->Host, Rd->RUri); + Rd->reply->addstrf(Rd->reply, "404 Unknown page" + "" + "You should get redirected to https://%s%s", + Rd->Host, Rd->RUri, Rd->Host, Rd->RUri, Rd->Host, Rd->RUri + ); + D("Redirecting dynamic page %s -> https://%s%s", file, Rd->Host, Rd->RUri); + return; + } + + // Check "Origin" header and /or HTTP_REFERER header. + // "Origin" is either HTTP_HOST or X-FORWARDED-HOST. Which could be "null". + char *ref = xmprintf("https://%s%s/account.html", getStrH(Rd->headers, "SERVER_NAME"), getStrH(Rd->headers, "SCRIPT_NAME")); + char *href = Rd->headers->getstr(Rd->headers, "HTTP_REFERER", true); + + if (NULL != href) + { + char *f = strchr(href, '?'); + + if (NULL != f) + *f = '\0'; + if (('\0' != href[0]) && (strcmp(ref, href) != 0)) + { + bitch(Rd, "Invalid referer.", ref); + D("Invalid referer - %s isn't %s", ref, href); + Rd->func = (pageBuildFunction) loginPage; + } + free(href); + } + free(ref); + ref = getStrH(Rd->headers, "SERVER_NAME"); + href = getStrH(Rd->headers, "HTTP_HOST"); + if ('\0' == href[0]) + href = getStrH(Rd->headers, "X-FORWARDED-HOST"); + if (('\0' != href[0]) && (strcmp(ref, href) != 0)) + { + bitch(Rd, "Invalid HOST.", ref); + D("Invalid HOST - %s isn't %s", ref, href); + Rd->func = (pageBuildFunction) loginPage; + } + + // Redirect to a GET if it was a POST. + if ((0 == e) && (strcmp("POST", Rd->Method) == 0)) + { + if (Rd->func == (pageBuildFunction) loginPage) + freeSesh(Rd, FALSE, TRUE); + + if (strcmp("confirm", doit) == 0) + { + createUser(Rd); + newSesh(Rd, TRUE); + Rd->chillOut = TRUE; + } + + if (strcmp("login", doit) == 0) + Rd->chillOut = TRUE; + + if (Rd->vegOut) + { + T("vegOut"); + freeSesh(Rd, FALSE, TRUE); + } + else if (Rd->chillOut) + { + T("chillOut"); + freeSesh(Rd, FALSE, FALSE); + newSesh(Rd, FALSE); + } + else if ('\0' == Rd->shs.leaf[0]) + newSesh(Rd, FALSE); + + Rd->Rheaders->putstr (Rd->Rheaders, "Status", "303 See Other"); + Rd->Rheaders->putstrf(Rd->Rheaders, "Location", "https://%s%s", Rd->Host, Rd->RUri); + Rd->reply->addstrf(Rd->reply, "Post POST redirect" + "" + "You should get redirected to https://%s%s", + Rd->Host, Rd->RUri, Rd->Host, Rd->RUri, Rd->Host, Rd->RUri + ); + I("Redirecting dynamic page %s -> https://%s%s", file, Rd->Host, Rd->RUri); + } + else // Actually send the page. + { + if (Rd->func == (pageBuildFunction) loginPage) + { + if (strcmp("confirm", doit) != 0) + freeSesh(Rd, FALSE, TRUE); + newSesh(Rd, FALSE); + } + Rd->func(Rd, ""); + } + + C("Ending dynamic page %s", file); +} + + +void sledjchisl_main(void) +//int main(int argc, char *argv[], char **env) +{ + // Don't segfault if our environment is crazy. +// if (!*argv) return 127; + + char *cmd = *toys.optargs; + + +// char *cmd = *argv; + char *tmp; + qhashtbl_t *configs = qhashtbl(0, 0); + lua_State *L = luaL_newstate(); + MYSQL *database = NULL, *dbconn = NULL; + gridStats *stats = NULL; + struct stat statbuf; + int status, result, i; + void *vd; + + pwd = getcwd(0, 0); + + if (-1 == fstat(STDIN_FILENO, &statbuf)) + { + error_msg("fstat() failed"); + if (1 != isatty(STDIN_FILENO)) + isWeb = TRUE; + } + else + { + if (S_ISREG (statbuf.st_mode)) D("regular file"); + else if (S_ISDIR (statbuf.st_mode)) D("directory"); + else if (S_ISCHR (statbuf.st_mode)) D("character device"); + else if (S_ISBLK (statbuf.st_mode)) D("block device"); + else if (S_ISFIFO(statbuf.st_mode)) D("FIFO (named pipe)"); + else if (S_ISLNK (statbuf.st_mode)) D("symbolic link"); + else if (S_ISSOCK(statbuf.st_mode)) D("socket"); + else D("unknown file descriptor type"); + if (!S_ISCHR(statbuf.st_mode)) + isWeb = TRUE; + } + if (!isWeb) + { + I("Outputting to a terminal, not a web server."); + // Check if we are already running inside the proper tmux server. + char *eTMUX = getenv("TMUX"); + memset(toybuf, 0, sizeof(toybuf)); + snprintf(toybuf, sizeof(toybuf), "%s/%s", scRoot, Tsocket); + if (((eTMUX) && (0 == strncmp(toybuf, eTMUX, strlen(toybuf))))) + { + I("Running inside the proper tmux server."); + isTmux = TRUE; + } + else + I("Not running inside the proper tmux server, starting it."); + I("libfcgi version: %s", FCGI_VERSION); + I("Lua version: %s", LUA_RELEASE); + I("LuaJIT version: %s", LUAJIT_VERSION); + I("MariaDB / MySQL client version: %s", mysql_get_client_info()); + } + + // Print our user name and groups. + struct passwd *pw; + struct group *grp; + uid_t euid = geteuid(); + gid_t egid = getegid(); + gid_t *groups = (gid_t *)toybuf; + i = sizeof(toybuf)/sizeof(gid_t); + int ngroups; + + pw = xgetpwuid(euid); + D("Running as user %s", pw->pw_name); + + grp = xgetgrgid(egid); + ngroups = getgroups(i, groups); + if (ngroups < 0) perror_exit("getgroups"); + D("User is in group %s", grp->gr_name); + for (i = 0; i < ngroups; i++) { + if (groups[i] != egid) + { + if ((grp = getgrgid(groups[i]))) + D("User is in group %s", grp->gr_name); + else + D("User is in group %u", groups[i]); + } + } + + +/* From http://luajit.org/install.html - +To change or extend the list of standard libraries to load, copy +src/lib_init.c to your project and modify it accordingly. Make sure the +jit library is loaded or the JIT compiler will not be activated. +*/ + luaL_openlibs(L); // Load Lua libraries. + + // Load the config scripts. + char *cPaths[] = + { + "/etc/sledjChisl.conf.lua", +// "/etc/sledjChisl.d/*.lua", + "~/.sledjChisl.conf.lua", +// "~/.config/sledjChisl/*.lua", + ".sledjChisl.conf.lua", + NULL + }; + struct stat st; + + + for (i = 0; cPaths[i]; i++) + { + memset(toybuf, 0, sizeof(toybuf)); + if (('/' == cPaths[i][0]) || ('~' == cPaths[i][0])) + snprintf(toybuf, sizeof(toybuf), "%s", cPaths[i]); + else + snprintf(toybuf, sizeof(toybuf), "%s/%s", pwd, cPaths[i]); + if (0 != lstat(toybuf, &st)) + continue; + if (!isWeb) I("Loading configuration file - %s", toybuf); + status = luaL_loadfile(L, toybuf); + if (status) // If something went wrong, error message is at the top of the stack. + E("Couldn't load file: %s", lua_tostring(L, -1)); + else + { + result = lua_pcall(L, 0, LUA_MULTRET, 0); + if (result) + E("Failed to run script: %s", lua_tostring(L, -1)); + else + { + lua_getglobal(L, "config"); + lua_pushnil(L); + while(lua_next(L, -2) != 0) + { + char *n = (char *) lua_tostring(L, -2); + + // Numbers can convert to strings, so check for numbers before checking for strings. + // On the other hand, strings that can be converted to numbers also pass lua_isnumber(). sigh + if (lua_isnumber(L, -1)) + { + float v = lua_tonumber(L, -1); + configs->put(configs, n, &v, sizeof(float)); + } + else if (lua_isstring(L, -1)) + configs->putstr(configs, n, (char *) lua_tostring(L, -1)); + else + { + char *v = (char *) lua_tostring(L, -1); + E("Unknown config variable type for %s = %s", n, v); + } + lua_pop(L, 1); + } + } + } + } + if ((vd = configs->get (configs, "loadAverageInc", NULL, false)) != NULL) {loadAverageInc = *((float *) vd); D("Setting loadAverageInc = %f", loadAverageInc);} + if ((vd = configs->get (configs, "simTimeOut", NULL, false)) != NULL) {simTimeOut = (int) *((float *) vd); D("Setting simTimeOut = %d", simTimeOut);} + if ((tmp = configs->getstr(configs, "scRoot", false)) != NULL) {scRoot = tmp; D("Setting scRoot = %s", scRoot);} + if ((tmp = configs->getstr(configs, "scUser", false)) != NULL) {scUser = tmp; D("Setting scUser = %s", scUser);} + if ((tmp = configs->getstr(configs, "Tconsole", false)) != NULL) {Tconsole = tmp; D("Setting Tconsole = %s", Tconsole);} + if ((tmp = configs->getstr(configs, "Tsocket", false)) != NULL) {Tsocket = tmp; D("Setting Tsocket = %s", Tsocket);} + if ((tmp = configs->getstr(configs, "Ttab", false)) != NULL) {Ttab = tmp; D("Setting Ttab = %s", Ttab);} + if ((tmp = configs->getstr(configs, "webRoot", false)) != NULL) {webRoot = tmp; D("Setting webRoot = %s", webRoot);} + if ((tmp = configs->getstr(configs, "URL", false)) != NULL) {URL = tmp; D("Setting URL = %s", URL);} + if ((vd = configs->get (configs, "seshTimeOut", NULL, false)) != NULL) {seshTimeOut = (int) *((float *) vd); D("Setting seshTimeOut = %d", seshTimeOut);} + if ((vd = configs->get (configs, "idleTimeOut", NULL, false)) != NULL) {idleTimeOut = (int) *((float *) vd); D("Setting idleTimeOut = %d", idleTimeOut);} + if ((vd = configs->get (configs, "newbieTimeOut", NULL, false)) != NULL) {newbieTimeOut = (int) *((float *) vd); D("Setting newbieTimeOut = %d", newbieTimeOut);} + + + if (isTmux || isWeb) + { + char *d; + + mimeTypes = qhashtbl(0, 0); + mimeTypes->putstr(mimeTypes, "gz", "application/gzip"); + mimeTypes->putstr(mimeTypes, "js", "application/javascript"); + mimeTypes->putstr(mimeTypes, "json", "application/json"); + mimeTypes->putstr(mimeTypes, "pdf", "application/pdf"); + mimeTypes->putstr(mimeTypes, "rtf", "application/rtf"); + mimeTypes->putstr(mimeTypes, "zip", "application/zip"); + mimeTypes->putstr(mimeTypes, "xz", "application/x-xz"); + mimeTypes->putstr(mimeTypes, "gif", "image/gif"); + mimeTypes->putstr(mimeTypes, "png", "image/png"); + mimeTypes->putstr(mimeTypes, "jp2", "image/jp2"); + mimeTypes->putstr(mimeTypes, "jpg2", "image/jp2"); + mimeTypes->putstr(mimeTypes, "jpe", "image/jpeg"); + mimeTypes->putstr(mimeTypes, "jpg", "image/jpeg"); + mimeTypes->putstr(mimeTypes, "jpeg", "image/jpeg"); + mimeTypes->putstr(mimeTypes, "svg", "image/svg+xml"); + mimeTypes->putstr(mimeTypes, "svgz", "image/svg+xml"); + mimeTypes->putstr(mimeTypes, "tif", "image/tiff"); + mimeTypes->putstr(mimeTypes, "tiff", "image/tiff"); + mimeTypes->putstr(mimeTypes, "css", "text/css"); + mimeTypes->putstr(mimeTypes, "html", "text/html"); + mimeTypes->putstr(mimeTypes, "htm", "text/html"); + mimeTypes->putstr(mimeTypes, "shtml", "text/html"); +// mimeTypes->putstr(mimeTypes, "md", "text/markdown"); +// mimeTypes->putstr(mimeTypes, "markdown", "text/markdown"); + mimeTypes->putstr(mimeTypes, "txt", "text/plain"); + + memset(toybuf, 0, sizeof(toybuf)); + snprintf(toybuf, sizeof(toybuf), "%s/config/config.ini", scRoot); + +// TODO - it looks like OpenSim invented their own half arsed backwards INI file include system. +// I doubt qlibc supports it, like it supports what seems to be the standard include system. +// Not sure if we need to worry about it just yet. + qlisttbl_t *qconfig = qconfig_parse_file(NULL, toybuf, '='); + if (NULL == qconfig) + { + E("Can't read config file %s", toybuf); + goto finished; + } + d = qstrunchar(qconfig->getstr(qconfig, "Const.ConnectionString", false), '"', '"'); + + if (NULL == d) + { + E("No database credentials in %s!", toybuf); + goto finished; + } + else + { + char *p0, *p1, *p2; + if (NULL == (d = strdup(d))) + { + E("Out of memory!"); + goto finished; + } + // Data Source=MYSQL_HOST;Database=MYSQL_DB;User ID=MYSQL_USER;Password=MYSQL_PASSWORD;Old Guids=true; + p0 = d; + while (NULL != p0) + { + p1 = strchr(p0, '='); + if (NULL == p1) break; + *p1 = '\0'; + p2 = strchr(p1 + 1, ';'); + if (NULL == p2) break; + *p2 = '\0'; + configs->putstr(configs, p0, p1 + 1); // NOTE - this allocs memory for it's key and it's data. + p0 = p2 + 1; + if ('\0' == *p0) + p0 = NULL; + }; + free(d); + } + +// TODO - should also load god names, and maybe the SMTP stuff. +// Note that the OpenSim SMTP stuff is ONLY for LSL script usage, we probably want to put it in the Lua config file instead. + + if (mysql_library_init(toys.optc, toys.optargs, NULL)) +// if (mysql_library_init(argc, argv, NULL)) + { + E("mysql_library_init() failed!"); + goto finished; + } + database = mysql_init(NULL); + if (NULL == database) + { + E("mysql_init() failed - %s", mysql_error(database)); + goto finished; + } + else + { + dbconn = mysql_real_connect(database, + configs->getstr(configs, "Data Source", true), + configs->getstr(configs, "User ID", true), + configs->getstr(configs, "Password", true), + configs->getstr(configs, "Database", true), +// 3036, "/var/run/mysqld/mysqld.sock", + 0, NULL, + CLIENT_FOUND_ROWS | CLIENT_LOCAL_FILES | CLIENT_MULTI_STATEMENTS | CLIENT_MULTI_RESULTS); + if (NULL == dbconn) + { + E("mysql_real_connect() failed - %s", mysql_error(database)); + goto finished; + } + + // Need to kick this off. + stats = getStats(database, stats); + char *h = qstrunchar(qconfig->getstr(qconfig, "Const.HostName", false), '"', '"'); + char *p = qstrunchar(qconfig->getstr(qconfig, "Const.PublicPort", false), '"', '"'); + stats->stats->putstr(stats->stats, "grid", qstrunchar(qconfig->getstr(qconfig, "Const.GridName", false), '"', '"')); + stats->stats->putstr(stats->stats, "HostName", h); + stats->stats->putstr(stats->stats, "PublicPort", p); + snprintf(toybuf, sizeof(toybuf), "http://%s:%s/", h, p); + + stats->stats->putstr(stats->stats, "uri", toybuf); + } + } + + + if (isWeb) + { + char **initialEnv = environ; + char *tmp0, *tmp1; + int count = 0, entries, bytes; + + dynPages = qhashtbl(0, 0); + newDynPage("account.html", (pageFunction) account_html); + + // FCGI_LISTENSOCK_FILENO is the socket to the web server. + // STDOUT and STDERR go to the web servers error log, or at least it does in Apache 2 mod_fcgid. + I("Running SledjChisl inside a web server, pid %d.", getpid()); + + if (0 == toys.optc) +// if (1 == argc) + D("no args"); + else + { + for (entries = 0, bytes = -1; entries < toys.optc; entries++) + D("ARG %s\n", toys.optargs[entries]); +// for (i = 0; argv[i] != NULL; i++) +// D("ARG %s", argv[i]); + } +// printEnv(env); + printEnv(environ); + +/* +? https://stackoverflow.com/questions/30707792/how-to-disable-buffering-with-apache2-and-mod-proxy-fcgi + https://z-issue.com/wp/apache-2-4-the-event-mpm-php-via-mod_proxy_fcgi-and-php-fpm-with-vhosts/ + A lengthy and detailed "how to set this up with PHP" that might be useful. + https://www.apachelounge.com/viewtopic.php?t=4385 + "Also the new mod_proxy_fcgi for Apache 2.4 seems to be crippled just like mod_fcgid in terms of being limited to just one request per process at a time." + But then so is the silly fcgi2 SDK, which basically assumes it's a CGI wrapper, not proper FCGI. ++ I could even start the spawn-fcgi process from the tmux instance of sledjchisl. ++ Orrr just open the socket / port myself from the tmux instance and do the raw FCGI thing through it. +*/ + while (FCGI_Accept() != -1) + { + reqData *Rd = xzalloc(sizeof(reqData)); + if (-1 == clock_gettime(CLOCK_REALTIME, &Rd->then)) + perror_msg("Unable to get the time."); + Rd->L = L; + Rd->configs = configs; +// Rd->queries = qhashtbl(0, 0); // Inited in toknize below. +// Rd->body = qhashtbl(0, 0); // Inited in toknize below. +// Rd->cookies = qhashtbl(0, 0); // Inited in toknize below. + Rd->headers = qhashtbl(0, 0); + Rd->stuff = qhashtbl(0, 0); + Rd->database = qhashtbl(0, 0); + Rd->Rcookies = qhashtbl(0, 0); + Rd->Rheaders = qhashtbl(0, 0); + Rd->db = database; + Rd->stats = stats; + Rd->errors = qlist(0); + Rd->messages = qlist(0); + Rd->reply = qgrow(QGROW_THREADSAFE); + qhashtbl_obj_t hobj; + qlist_obj_t lobj; + + // So far I'm seeing these as all caps, but I don't think they are defined that way. Case insensitive per the spec. + // So convert them now, also "-" -> "_". +T("HEADERS"); + char **envp = environ; + for ( ; *envp != NULL; envp++) + { + char *k = xstrdup(*envp); + char *v = strchr(k, '='); + if (NULL != v) + { + *v = '\0'; + char *ky = qstrreplace("tr", qstrupper(k), "-", "_"); + Rd->headers->putstr(Rd->headers, ky, v + 1); +if ((strcmp("HTTP_COOKIE", ky) == 0) || (strcmp("CONTENT_LENGTH", ky) == 0) || (strcmp("QUERY_STRING", ky) == 0)) + D(" %s = %s", ky, v + 1); + } + } + + // The FCGI paramaters sent from the server, are converted to environment variablse for the fcgi2 SDK. + // The FCGI spec doesn't mention what these are. except FCGI_WEB_SERVER_ADDRS. + char *Role = Rd->headers->getstr(Rd->headers, "FCGI_ROLE", false); + char *Path = Rd->headers->getstr(Rd->headers, "PATH_INFO", false); +// if (NULL == Path) {msleep(1000); continue;} + char *Length = Rd->headers->getstr(Rd->headers, "CONTENT_LENGTH", false); +//char *Type = Rd->headers->getstr(Rd->headers, "CONTENT_TYPE", false); + Rd->Method = Rd->headers->getstr(Rd->headers, "REQUEST_METHOD", false); + Rd->Script = Rd->headers->getstr(Rd->headers, "SCRIPT_NAME", false); + Rd->Scheme = Rd->headers->getstr(Rd->headers, "REQUEST_SCHEME", false); + Rd->Host = Rd->headers->getstr(Rd->headers, "HTTP_HOST", false); +//char *SUri = Rd->headers->getstr(Rd->headers, "SCRIPT_URI", false); + Rd->RUri = Rd->headers->getstr(Rd->headers, "REQUEST_URI", false); +//char *Cookies = Rd->headers->getstr(Rd->headers, "HTTP_COOKIE", false); +//char *Referer = Rd->headers->getstr(Rd->headers, "HTTP_REFERER", false); +//char *RAddr = Rd->headers->getstr(Rd->headers, "REMOTE_ADDR", false); +//char *Cache = Rd->headers->getstr(Rd->headers, "HTTP_CACHE_CONTROL", false); +//char *FAddrs = Rd->headers->getstr(Rd->headers, "FCGI_WEB_SERVER_ADDRS", false); +//char *Since = Rd->headers->getstr(Rd->headers, "IF_MODIFIED_SINCE", false); + /* Per the spec CGI https://www.ietf.org/rfc/rfc3875 + meta-variable-name = "AUTH_TYPE" | "CONTENT_LENGTH" | + "CONTENT_TYPE" | "GATEWAY_INTERFACE" | + "PATH_INFO" | "PATH_TRANSLATED" | + "QUERY_STRING" | "REMOTE_ADDR" | + "REMOTE_HOST" | "REMOTE_IDENT" | + "REMOTE_USER" | "REQUEST_METHOD" | + "SCRIPT_NAME" | "SERVER_NAME" | + "SERVER_PORT" | "SERVER_PROTOCOL" | + "SERVER_SOFTWARE" + Also protocol / scheme specific ones - + HTTP_* comes from some of the request header. The rest are likely part of the other env variables. + Also covers HTTPS headers, with the HTTP_* names. + + */ + +T("COOKIES"); + Rd->cookies = toknize(Rd->headers->getstr(Rd->headers, "HTTP_COOKIE", false), "=;"); + santize(Rd->cookies, TRUE); +T("QUERY"); + Rd->queries = toknize(Rd->headers->getstr(Rd->headers, "QUERY_STRING", false), "=&"); + santize(Rd->queries, TRUE); + char *Body = NULL; + if (Length != NULL) + { + size_t len = strtol(Length, NULL, 10); + Body = xmalloc(len + 1); + int c = FCGI_fread(Body, 1, len, FCGI_stdin); + if (c != len) + { + E("Tried to read %d of the body, only got %d!", len, c); + } + Body[len] = '\0'; + } +T("BODY"); + Rd->body = toknize(Body, "=&"); + santize(Rd->body, TRUE); + + + I("Started SledjChisl FCGI web %s request ROLE = %s, body is %s bytes, pid %d.", Rd->Method, Role, Length, getpid()); + I(" %s://%s%s -> %s/html%s", Rd->Scheme, Rd->Host, Rd->RUri, webRoot, Path); + + +/* TODO - other headers may include - + different Content-type + Status: 304 Not Modified + Last-Modified: timedatemumble + https://en.wikipedia.org/wiki/List_of_HTTP_header_fields +*/ + Rd->Rheaders->putstr(Rd->Rheaders, "Status", "200 OK"); + Rd->Rheaders->putstr(Rd->Rheaders, "Content-type", "text/html"); +// TODO - check these. + // This is all dynamic web pages, and likeley secure to. + // Most of these are from https://www.smashingmagazine.com/2017/04/secure-web-app-http-headers/ + // https://www.twilio.com/blog/a-http-headers-for-the-responsible-developer is good to, and includes useful compression and image stuff. + // On the other hand, .css files are referenced, which might be better off being cached, so should tweak some of thees. + Rd->Rheaders->putstr(Rd->Rheaders, "Cache-Control", "no-cache, no-store, must-revalidate"); + Rd->Rheaders->putstr(Rd->Rheaders, "Pragma", "no-cache"); + Rd->Rheaders->putstr(Rd->Rheaders, "Expires", "-1"); +// Rd->Rheaders->putstr(Rd->Rheaders, "Content-Security-Policy", "script-src 'self'"); // This can get complex. + Rd->Rheaders->putstr(Rd->Rheaders, "X-XSS-Protection", "1;mode=block"); + Rd->Rheaders->putstr(Rd->Rheaders, "X-Frame-Options", "SAMEORIGIN"); + Rd->Rheaders->putstr(Rd->Rheaders, "X-Content-Type-Options", "nosniff"); +// Failed experiment. +// Rd->Rheaders->putstr(Rd->Rheaders, "X-Toke-N-Munchie", "foo, bar"); + + if ((strcmp("GET", Rd->Method) != 0) && (strcmp("HEAD", Rd->Method) != 0) && (strcmp("POST", Rd->Method) != 0)) + { + E("Unsupported HTTP method %s", Rd->Method); + Rd->Rheaders->putstr(Rd->Rheaders, "Status", "405 Method Not Allowed"); + goto sendReply; + } + + memset(toybuf, 0, sizeof(toybuf)); + snprintf(toybuf, sizeof(toybuf), "%s/html%s", webRoot, Path); + HTMLfile *thisFile = checkHTMLcache(toybuf); + if (NULL == thisFile) + { + dynPage *dp = dynPages->get(dynPages, &Path[1], NULL, false); + if (NULL == dp) + { + E("Can't access file %s", toybuf); + Rd->Rheaders->putstr(Rd->Rheaders, "Status", "404 Not Found"); + E("Failed to open %s, it's not a virtual file either", toybuf); + goto sendReply; + } + I("Dynamic page %s found.", dp->name); + dp->func(toybuf, Rd, thisFile); + char *finl = Rd->reply->tostring(Rd->reply); // This mallocs new storage and returns it to us. +// TODO - maybe cache this? + qlist_t *fragments = fragize(finl, Rd->reply->datasize(Rd->reply)); + Rd->reply->free(Rd->reply); + Rd->reply = qgrow(QGROW_THREADSAFE); + unfragize(fragments, Rd); +goto sendReply; + } + + tmp0 = qfile_get_ext(toybuf); + tmp1 = mimeTypes->getstr(mimeTypes, tmp0, false); + if (NULL != tmp1) + { + if (strncmp("text/", tmp1, 5) != 0) + { + E("Only text formats are supported - %s", toybuf); + Rd->Rheaders->putstr(Rd->Rheaders, "Status", "415 Unsupported Media Type"); + goto sendReply; + } + } + else + { + E("Not actually a teapot, er I mean file has no extension, can't determine media type the easy way - %s", toybuf); + Rd->Rheaders->putstr(Rd->Rheaders, "Status", "418 I'm a teapot"); + goto sendReply; + } + +// Rd->Rheaders->putstr(Rd->Rheaders, "Last-Modified", thisFile->last.tv_sec); +// This is dynamic content, it's always gonna be modified. I think. +// if (NULL != Since) +// { +// time_t snc = qtime_parse_gmtstr(Since); +// TODO - should validate the time, log and ignore it if not valid. +// if (thisFile->last.tv_sec < snc) +// { +// D("Status: 304 Not Modified - %s", toybuf); +// setHeader("Status", "304 Not Modified"); +// goto sendReply; +// } +// } + + if (strcmp("HEAD", Rd->Method) == 0) + goto sendReply; + + getStats(database, stats); + unfragize(thisFile->fragments, Rd); + +sendReply: + /* Send headers. + BTW, the Status header should be sent first I think. + https://www.ietf.org/rfc/rfc3875 6.2 says order isn't important. + It even says Status is optional, 200 is assumed. Content-Type is mandatory. + 8.2 "Recommendations for Scripts" is worth complying with. + 9 "Security Considerations" + https://tools.ietf.org/html/rfc7230 3.1.2 says status line must be first. lol + */ + FCGI_printf("Status: %s\r\n", getStrH(Rd->Rheaders, "Status")); + memset((void *) &hobj, 0, sizeof(hobj)); + Rd->Rheaders->lock(Rd->Rheaders); + while (Rd->Rheaders->getnext(Rd->Rheaders, &hobj, false) == true) + { + if (strcmp("Status", (char *) hobj.name) != 0) + FCGI_printf("%s: %s\r\n", (char *) hobj.name, (char *) hobj.data); + } + Rd->Rheaders->unlock(Rd->Rheaders); + // Send cookies. + memset((void *) &hobj, 0, sizeof(hobj)); + Rd->Rcookies->lock(Rd->Rcookies); + while (Rd->Rcookies->getnext(Rd->Rcookies, &hobj, false) == true) + { + cookie *ck = (cookie *) hobj.data; + FCGI_printf("Set-Cookie: %s=%s", ck->cookie, ck->value); +// if (NULL != ck->expires) FCGI_printf("; Expires=%s", ck->expires); + if (NULL != ck->domain) FCGI_printf("; Domain=%s", ck->domain); + if (NULL != ck->path) FCGI_printf("; Path=%s", ck->path); + if (0 != ck->maxAge) FCGI_printf("; Max-Age=%d", ck->maxAge); + if ( ck->secure) FCGI_printf("; Secure"); + if ( ck->httpOnly) FCGI_printf("; HttpOnly"); + if (CS_STRICT == ck->site) FCGI_printf("; SameSite=Strict"); + if (CS_LAX == ck->site) FCGI_printf("; SameSite=Lax"); + if (CS_NONE == ck->site) FCGI_printf("; SameSite=None"); + FCGI_printf("\r\n"); + free(ck->value); + free(ck->cookie); + } + FCGI_printf("\r\n"); + Rd->cookies->unlock(Rd->cookies); + // Send body. + char *final = Rd->reply->tostring(Rd->reply); + if (NULL == final) + { + tmp0 = Rd->Rheaders->getstr(Rd->Rheaders, "Status", false); + if (NULL == tmp0) + { + E("Some sort of error happpened! Status: UNKNOWN!!"); + FCGI_printf("Some sort of error happpened! Status: UNKNOWN!!"); + } + else + { + E("Some sort of error happpened! Status: %s", tmp0); + FCGI_printf("Some sort of error happpened! Status: %s", tmp0); + } + } + else + { + FCGI_printf("%s", final); + free(final); + } + +fcgiDone: + FCGI_Finish(); + qgrow_free(Rd->reply); + qlist_free(Rd->messages); + qlist_free(Rd->errors); + qhashtbl_free(Rd->Rheaders); + qhashtbl_free(Rd->Rcookies); + qhashtbl_free(Rd->database); + qhashtbl_free(Rd->stuff); + qhashtbl_free(Rd->headers); + qhashtbl_free(Rd->cookies); + qhashtbl_free(Rd->body); + qhashtbl_free(Rd->queries); + + struct timespec now; + if (-1 == clock_gettime(CLOCK_REALTIME, &now)) + perror_msg("Unable to get the time."); + double n = (now.tv_sec * 1000000000.0) + now.tv_nsec; + double t = (Rd->then.tv_sec * 1000000000.0) + Rd->then.tv_nsec; + I("Finished SledjChisl web request, took %lf seconds", (n - t) / 1000000000.0); + } + + FCGI_fprintf(FCGI_stderr, "Stopped SledjChisl web server.\n"); + D("Stopped SledjChisl web server."); + + goto finished; + } + + + if (!isTmux) + { // Let's see if the proper tmux server is even running. + memset(toybuf, 0, sizeof(toybuf)); + snprintf(toybuf, sizeof(toybuf), "%s %s/%s -q list-sessions 2>/dev/null | grep -q %s:", Tcmd, scRoot, Tsocket, Tconsole); + i = system(toybuf); + if (WIFEXITED(i)) + { + if (0 != WEXITSTATUS(i)) // No such sesion, create it. + { + memset(toybuf, 0, sizeof(toybuf)); + // The sudo is only so that the session is owned by opensim, otherwise it's owned by whoever ran this script, which is a likely security hole. + // After the session is created, we rely on the caches directory to be group sticky, so that anyone in the opensim group can attach to the tmux socket. + snprintf(toybuf, sizeof(toybuf), + "sudo -Hu %s %s %s/%s new-session -d -s %s -n '%s' \\; split-window -bhp 50 -t '%s:' bash -c './sledjchisl; cd %s; bash'", + scUser, Tcmd, scRoot, Tsocket, Tconsole, Ttab, Tconsole, scRoot); + i = system(toybuf); + if (!WIFEXITED(i)) + E("tmux new-session command failed!"); + } + // Join the session. + memset(toybuf, 0, sizeof(toybuf)); + snprintf(toybuf, sizeof(toybuf), "%s %s/%s select-window -t '%s' \\; attach-session -t '%s'", Tcmd, scRoot, Tsocket, Tconsole, Tconsole); + i = system(toybuf); + if (!WIFEXITED(i)) + E("tmux attach-session command failed!"); + goto finished; + } + else + E("tmux list-sessions command failed!"); + } + + + + simList *sims = getSims(); + if (1) + { + struct sysinfo info; + float la; + + sysinfo(&info); + la = info.loads[0]/65536.0; + + if (!checkSimIsRunning("ROBUST")) + { + char *d = xmprintf("%s.{right}", Ttab); + char *c = xmprintf("cd %s/current/bin", scRoot); + + I("ROBUST is starting up."); + sendTmuxCmd(d, c); + free(c); + c = xmprintf("mono Robust.exe -inidirectory=%s/config/ROBUST", scRoot); + sendTmuxCmd(d, c); + free(c); + waitTmuxText(d, "INITIALIZATION COMPLETE FOR ROBUST"); + I("ROBUST is done starting up."); + la = waitLoadAverage(la, loadAverageInc / 3.0, simTimeOut / 3); + free(d); + } + +// for (i = 0; i < sims->num; i++) + for (i = 0; i < 2; i++) + { + char *sim = sims->sims[i], *name = getSimName(sims->sims[i]); + + if (!checkSimIsRunning(sim)) + { + I("%s is starting up.", name); + memset(toybuf, 0, sizeof(toybuf)); + snprintf(toybuf, sizeof(toybuf), "%s %s/%s new-window -dn '%s' -t '%s:%d' 'cd %s/current/bin; mono OpenSim.exe -inidirectory=%s/config/%s'", + Tcmd, scRoot, Tsocket, name, Tconsole, i + 1, scRoot, scRoot, sim); + int r = system(toybuf); + if (!WIFEXITED(r)) + E("tmux new-window command failed!"); + else + { + memset(toybuf, 0, sizeof(toybuf)); + snprintf(toybuf, sizeof(toybuf), "INITIALIZATION COMPLETE FOR %s", name); + waitTmuxText(name, toybuf); + I("%s is done starting up.", name); + la = waitLoadAverage(la, loadAverageInc, simTimeOut); + } + } + } + + } + else if (!strcmp(cmd, "create")) // "create name x,y size" + { + } + else if (!strcmp(cmd, "start")) // "start sim01" "start Welcome" "start" start everything + { + } + else if (!strcmp(cmd, "backup")) // "backup onefang rejected" "backup sim01" "backup Welcome" "backup" backup everything + { // If it's not a sim code, and not a sim name, it's an account inventory. + } + else if (!strcmp(cmd, "gitAR")) // "gitAR i name" + { + } + else if (!strcmp(cmd, "stop")) // "stop sim01" "stop Welcome" "stop" stop everything + { + } + + + double sum; + + // Load the file containing the script we are going to run + status = luaL_loadfile(L, "script.lua"); + if (status) + { + // If something went wrong, error message is at the top of the stack + E("Couldn't load file: %s", lua_tostring(L, -1)); + goto finished; + } + + /* + * Ok, now here we go: We pass data to the lua script on the stack. + * That is, we first have to prepare Lua's virtual stack the way we + * want the script to receive it, then ask Lua to run it. + */ + lua_newtable(L); /* We will pass a table */ + + /* + * To put values into the table, we first push the index, then the + * value, and then call lua_rawset() with the index of the table in the + * stack. Let's see why it's -3: In Lua, the value -1 always refers to + * the top of the stack. When you create the table with lua_newtable(), + * the table gets pushed into the top of the stack. When you push the + * index and then the cell value, the stack looks like: + * + * <- [stack bottom] -- table, index, value [top] + * + * So the -1 will refer to the cell value, thus -3 is used to refer to + * the table itself. Note that lua_rawset() pops the two last elements + * of the stack, so that after it has been called, the table is at the + * top of the stack. + */ + for (i = 1; i <= 5; i++) + { + lua_pushnumber(L, i); // Push the table index + lua_pushnumber(L, i*2); // Push the cell value + lua_rawset(L, -3); // Stores the pair in the table + } + + // By what name is the script going to reference our table? + lua_setglobal(L, "foo"); + + // Ask Lua to run our little script + result = lua_pcall(L, 0, LUA_MULTRET, 0); + if (result) + { + E("Failed to run script: %s", lua_tostring(L, -1)); + goto finished; + } + + // Get the returned value at the top of the stack (index -1) + sum = lua_tonumber(L, -1); + + I("Script returned: %.0f", sum); + + lua_pop(L, 1); // Take the returned value out of the stack + + // An example of calling a toy directly. +// printf("\n\n"); +// char *argv[] = {"ls", "-l", "-a", NULL}; +// printf("%d\n", runToy(argv)); + + + puts(""); + fflush(stdout); + +finished: + if (database) mysql_close(database); + mysql_library_end(); + lua_close(L); + if (stats) + { + if (stats->stats) stats->stats->free(stats->stats); + free(stats); + } + if (configs) configs->free(configs); +// return EXIT_SUCCESS; +} -- cgit v1.1