aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/src
diff options
context:
space:
mode:
authoronefang2020-03-16 20:22:12 +1000
committeronefang2020-03-16 20:22:12 +1000
commit3f67f0304f2138a38cf2695afec163b12db02d2c (patch)
tree3400e601150a5a8910b1e0f4aea8baf328f43d45 /src
parentShift web stuff to the www folder, and update it. (diff)
downloadopensim-SC-3f67f0304f2138a38cf2695afec163b12db02d2c.zip
opensim-SC-3f67f0304f2138a38cf2695afec163b12db02d2c.tar.gz
opensim-SC-3f67f0304f2138a38cf2695afec163b12db02d2c.tar.bz2
opensim-SC-3f67f0304f2138a38cf2695afec163b12db02d2c.tar.xz
Finally add the sledjchisl C & Lua maanger, and friends.
Diffstat (limited to 'src')
-rw-r--r--src/.sledjChisl.conf.lua26
-rwxr-xr-xsrc/BuildIt.sh140
-rw-r--r--src/miniconfig1
-rw-r--r--src/sledjchisl/NOTES.txt503
-rw-r--r--src/sledjchisl/README51
-rw-r--r--src/sledjchisl/fcgi_SC.c13
-rw-r--r--src/sledjchisl/fcgi_SC.h136
-rw-r--r--src/sledjchisl/script.lua18
-rw-r--r--src/sledjchisl/sledjchisl.c5080
9 files changed, 5948 insertions, 20 deletions
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 @@
1-- sledjChislConfig.lua
2
3-- This works coz LuaJIT automatically loads the jit module.
4if type(jit) == 'table' then
5 io.write('.sledjChisl.conf.lua is being run by ' .. jit.version .. ' under ' .. jit.os .. ' on a ' .. jit.arch .. '\n')
6else
7 io.write('.sledjChisl.conf.lua is being run by Lua version ' .. _VERSION .. '\n')
8end
9
10config =
11{
12 ["scRoot"] = "/opt/opensim_SC";
13 ["scUser"] = "opensimsc";
14 ["Tconsole"] = "SledjChisl";
15 ["Tsocket"] = "caches/opensim-tmux.socket";
16 ["Ttab"] = "SC";
17 ["loadAverageInc"] = 0.7;
18 ["simTimeOut"] = 45; -- seconds
19 ["webRoot"] = "/var/www";
20 ["URL"] = "sledjchisl.fcgi";
21 ["seshTimeOut"] = 30 * 60; -- seconds
22 ["idleTimeOut"] = 24 * 60 * 60; -- seconds
23 ["newbieTimeOut"] = 30; -- days
24 ["pepper"] = "My long beard is salt and pepper coloured, though there are no birds in it, only breakfast.";
25}
26return 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 @@
1#!/bin/bash 1#!/bin/bash
2 2
3mkdir -p build
4
5
3# Poor mans git sub modules / subtrees, coz otherwise it gets complex. 6# Poor mans git sub modules / subtrees, coz otherwise it gets complex.
7if [ ! -d git-sub-modules/fcgi2 ]; then
8 pushd git-sub-modules >/dev/null
9 git clone https://github.com/FastCGI-Archives/fcgi2.git
10 popd >/dev/null
11else
12 pushd git-sub-modules/fcgi2 >/dev/null
13 echo "Updating fcgi2."
14 git pull | grep "Already up-to-date." && rm -fr build/fcgi2
15 popd >/dev/null
16fi
17
18if [ ! -d build/fcgi2 ]; then
19 cp -r git-sub-modules/fcgi2 build/
20 pushd build/fcgi2 >/dev/null
21 make distclean
22 ./autogen.sh
23 ./configure
24 sed -e "s/#define PACKAGE/#define FCGI_PACKAGE/g" -i fcgi_config.h
25 sed -e "s/#define VERSION /#define FCGI_VERSION /g" -i fcgi_config.h
26 make
27 popd >/dev/null
28fi
29
30echo ""
31echo ""
32echo ""
33
34if [ ! -d git-sub-modules/luajit ]; then
35 pushd git-sub-modules >/dev/null
36 git clone https://luajit.org/git/luajit-2.0.git
37 mv luajit-2.0 luajit
38 pushd luajit >/dev/null
39 git checkout v2.1
40 popd >/dev/null
41 popd >/dev/null
42else
43 pushd git-sub-modules/luajit >/dev/null
44 echo "Updating LuaJIT."
45 git pull | grep "Already up-to-date." && rm -fr build/luajit
46 popd >/dev/null
47fi
48
49if [ ! -d build/luajit ]; then
50 rm -fr build/luajit
51 cp -r git-sub-modules/luajit build/
52
53 pushd build/luajit >/dev/null
54 make clean
55 make amalg
56 popd >/dev/null
57fi
58
59echo ""
60echo ""
61echo ""
62
63if [ ! -d git-sub-modules/qlibc ]; then
64 pushd git-sub-modules >/dev/null
65 git clone https://github.com/wolkykim/qlibc.git
66 popd >/dev/null
67else
68 pushd git-sub-modules/qlibc >/dev/null
69 echo "Updating qlibc."
70 git pull | grep "Already up-to-date." && rm -fr build/qlibc
71 popd >/dev/null
72fi
73
74if [ ! -d build/qlibc ]; then
75 rm -fr build/qlibc
76 cp -r git-sub-modules/qlibc build/
77
78 pushd build/qlibc >/dev/null
79 make clean
80 ./configure
81 make
82 popd >/dev/null
83fi
84
85echo ""
86echo ""
87echo ""
88
4if [ ! -d git-sub-modules/toybox ]; then 89if [ ! -d git-sub-modules/toybox ]; then
5 pushd git-sub-modules 90 pushd git-sub-modules >/dev/null
6 git clone https://github.com/landley/toybox.git 91 git clone https://github.com/landley/toybox.git
7 popd 92 popd >/dev/null
8else 93else
9 pushd git-sub-modules/toybox 94 pushd git-sub-modules/toybox >/dev/null
10 git pull 95 echo "Updating toybox."
11 popd 96 git pull | grep "Already up-to-date." && rm -fr build/toybox
97 popd >/dev/null
12fi 98fi
13 99
14pushd git-sub-modules/toybox 100if [ ! -d build/toybox ]; then
15#git stash save 101 rm -fr build/toybox
16#git pull 102 cp -r git-sub-modules/toybox build/
17#git stash pop 103 ln -fs ../../../boxes build/toybox/toys/boxes
18popd 104 ln -fs ../../../sledjchisl build/toybox/toys/sledjchisl
105 ln -fs ../toys/sledjchisl/fcgi_SC.c build/toybox/lib
106 ln -fs ../toys/sledjchisl/fcgi_SC.h build/toybox/lib
107 ln -fs ../toys/boxes/handlekeys.c build/toybox/lib
108 ln -fs ../toys/boxes/handlekeys.h build/toybox/lib
19 109
20mkdir -p build 110 pushd build/toybox >/dev/null
21rm -fr build/toybox 111 sed -e "s/strend(/tb_strend(/g" -i lib/lib.h
22cp -r git-sub-modules/toybox build/ 112 find ./ -type f -name "*.c" -exec sed -e "s/strend(/tb_strend(/g" -i {} \;
23ln -fs ../../../boxes build/toybox/toys/boxes 113 make clean
24ln -fs ../toys/boxes/handlekeys.c build/toybox/lib 114 #make defconfig
25ln -fs ../toys/boxes/handlekeys.h build/toybox/lib 115 #make menuconfig
116 make allnoconfig KCONFIG_ALLCONFIG=../../miniconfig || exit 1
117 popd >/dev/null
118fi
119
120echo ""
121echo ""
122echo ""
26 123
27pushd build/toybox >/dev/null 124pushd build/toybox >/dev/null
28make clean 125export CFLAGS="-I../luajit/src -I../fcgi2 -I../fcgi2/include -I../qlibc/include/qlibc $(mysql_config --cflags) -g3"
29#make defconfig 126export 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"
30##make menuconfig
31make allnoconfig KCONFIG_ALLCONFIG=../../miniconfig || exit 1
32make || exit 1 127make || exit 1
33popd >/dev/null 128popd >/dev/null
129
130
131sudo rm -f /opt/opensim_SC/caches/sledjchisl.socket
132sudo 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
133#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
5CONFIG_SH=y 5CONFIG_SH=y
6CONFIG_SU=y 6CONFIG_SU=y
7CONFIG_UUIDGEN=y 7CONFIG_UUIDGEN=y
8CONFIG_SLEDJCHISL=y
8CONFIG_TOYBOX_FREE=y 9CONFIG_TOYBOX_FREE=y
9CONFIG_TOYBOX_HELP=y 10CONFIG_TOYBOX_HELP=y
10CONFIG_TOYBOX_HELP_DASHDASH=y 11CONFIG_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 @@
1I'm re-purposing this for SledjHamr https://sledjhamr.org/git/docs/index.html
2
3The general structure of SledjHamr is a bunch of servers talking to each
4other via Internet (or just local) connections. One of them is a web
5server for assets, world data, and inventory.
6
7Originally I didn't think using a web based world client was a good idea,
8however it might be better to have one, for reasons. Now I need a web
9management console that can do all the things the current tmux console
10can, including OpenSim console and commands. Plus account management for
11users. I can also use a web based Jabber / XMPP front end to chat, IM,
12and group chatter, which would run in the normal viewers web browser.
13This provides a doorway into putting SledjHamr stuff in existing viewers
14without needing them to support it. So a web based viewer now makes more
15sense, and also means we can get away with not needing a viewer at all.
16
17Toybox itself doesn't include a web server, and I don't think there is
18one on the roadmap. So we have to use an external web server, which was
19a design goal of SledjHamr in the first place, using existing mature
20HTTP infrastructure, coz that's already solved problems for a bunch of
21things that plague OS/SL to this day. Clear your cache! Pffft.
22
23So sledjchisl.c will be the "love world server", though initially it just
24drives OpenSim_SC in tmux via tmux commands to send keys and read output.
25Later it might run opensim_SC directly and use STDIN and STDOUT to do
26everything. It'll also provide the text management front end that runs
27in the left tmux panel of the first window, which is why it's based on
28boxes in the first place. Later still it can take over opensim_SC
29functions as I move them out of mono.
30
31We will need a text, web, and GUI version of this management front end.
32Hmmm, maybe don't need a GUI version, GUI users can just run a terminal.
33
34After much research, FastCGI / FCGI seems to be the most portable way of
35interfacing with existing web servers. FCGI protocol closes STDERR and
36STDOUT, and uses STDIN as it's two way communications channel to the web
37server, so our FCGI module can't be used as the text management front
38end. This is probably a good idea to keep them seperate anyway, for
39security, coz the web server is exposed to the world, the console isn't.
40
41Currently sledjchisl.c tests to see if it's running in tmux already, if
42it isn't it starts up tmux runs itself into this new tmux, then exits.
43So it could also test if it's running from FCGI, and switch to web mode,
44then it'll need to find the tmuxed instance to send commands to it.
45Either via nails connection, or sending tmux commands via shell.
46
47FCGI has methods of dealing with auth and templates. B-)
48
49So for now I think I'll have the text and web management front ends in
50sledjchisl.c, and the love world server as well. I can split them up
51later if I need to.
52
53
54
55
56I has Apache 2.4.25-3+deb9u9
57 MariaDB 10.1.44-MariaDB
58
59
60https://gist.github.com/dermesser/e2f9b66457ae19ebd116
61 Multithreaded example in C.
62
63
64-------------------------------------------------------------------
65
66Apache doesn't seem to support FCGI filter role, so I might have to do
67without. Might be better anyway.
68
69
70"A Filter is similar in functionality to a Responder that takes a data
71file as a parameter. The difference is that with a Filter, both the data
72file and the Filter itself can be access controlled using the Web
73server's access control mechanisms, while a Responder that takes the name
74of a data file as a parameter must perform its own access control checks
75on the data file."
76
77 Which is fine, our access control checks will be "Is this database
78 defined user already logged on via our FCGI script?". We should have
79 total control over that. I was planning on using the FCGI auth
80 mechanism anyway.
81
82
83RESPONDER
84 web server sends FCGI_PARAMS
85 CONTENT_LENGTH
86 web server sends input body FCGI_STDIN
87 fcgi app sends result data over FCGI_STDOUT and error messages over FCGI_STDERR
88 it has to finish reading FCGI_PARAMS first
89 fcgi app sends FCGI_END_REQUEST(protocolStatus = FCGI_REQUEST_COMPLETE)
90
91
92FILTER
93 filtered file has last modified time
94 web server sets FCGI_DATA_LAST_MOD accordingly
95 web server sends FCGI_PARAMS
96 CONTENT_LENGTH FCGI_DATA_LAST_MOD FCGI_DATA_LENGTH
97 web server sends input body FCGI_STDIN
98 web servers sends file over FCGI_DATA
99 fcgi app can ignore FCGI_DATA and use it's own cached copy based on FCGI_DATA_LAST_MOD
100 fcgi app sends result data over FCGI_STDOUT and error messages over FCGI_STDERR
101 it has to finish reading FCGI_STDIN first, but not FCGI_DATA
102 fcgi app sends FCGI_END_REQUEST(protocolStatus = FCGI_REQUEST_COMPLETE)
103
104
105Soooo, FILTER might be slower anyway if we are caching the filtered file,
106or mmapping it, coz filter has to start sending the filtered file, even
107if it's to be served from cache. Plus no need to wait for FCGI_STDIN
108before spewing it out.
109
110
111Last update time for parameters, plus an update frequency. Once a minute.
112
113 NOTE - SSI is a bit more complex than what I'm currently using.
114 https://en.wikipedia.org/wiki/Server_Side_Includes
115 <!--#include virtual="menu.cgi" -->
116 <!--#include file="footer.html" -->
117 <!--#exec cgi="/cgi-bin/foo.cgi" -->
118 <!--#exec cmd="ls -l" -->
119. <!--#echo var="REMOTE_ADDR" -->
120 <!--#config timefmt="%y %m %d" -->
121 <!--#config sizefmt="bytes" -->
122 <!--#config errmsg="SSI command failed!" -->
123 <!--#flastmod virtual="index.html" -->
124 <!--#fsize file="script.pl" -->
125 <!--#if expr="${Sec_Nav}" -->
126 <!--#include virtual="secondary_nav.txt" -->
127 <!--#elif expr="${Pri_Nav}" -->
128 <!--#include virtual="primary_nav.txt" -->
129 <!--#else -->
130 <!--#include virtual="article.txt" -->
131 <!--#endif -->
132 <!--#set var="foo" value="bar" -->
133 <!--#printenv -->
134 https://www.w3.org/Jigsaw/Doc/User/SSI.html
135 Adds lots of others, including Java stuff.
136 Mine
137 <!--#lua lua="print(table[key])" -->
138 <!--#lua file="/path/to/script.lua" -->
139 <!--#lua virtual="https://example.com/script.lua" -->
140
141-------------------------------------------------------------------
142
143Account creation process in the database.
144
145Apart from the usual input validation of things...
146
147
148OpenSim/Server/Handlers/UserAccounts/UserAccountServerPostHandler.cs
149 byte[] CreateUser(Dictionary<string, object> request)
150 Looks like their built in web front end, perhaps what is triggered by the console?
151 createdUserAccount
152 = ((UserAccountService)m_UserAccountService).CreateUser(scopeID, principalID, firstName, lastName, password, email, model);
153
154OpenSim/opensim-SC/OpenSim/ApplicationPlugins/RemoteController/RemoteAdminPlugin.cs
155 An XML RPC interface to -
156 private UserAccount CreateUser(UUID scopeID, string firstName, string lastName, string password, string email)
157 account = new UserAccount(scopeID, UUID.Random(), firstName, lastName, email);
158 if (userAccountService.StoreUserAccount(account))
159 success = authenticationService.SetPassword(account.PrincipalID, password)
160 gridUserService.SetHome(account.PrincipalID.ToString(), home.RegionID, new Vector3(128, 128, 0), new Vector3(0, 1, 0));
161 success = inventoryService.CreateUserInventory(account.PrincipalID);
162
163OpenSim/opensim-SC/OpenSim/Services/UserAccountService/UserAccountService.cs
164 Looks like the console command handler.
165 create user [<first> [<last> [<pass> [<email> [<user id> [<model>]]]]]] - Create a new user
166 protected void HandleCreateUser(string module, string[] cmdparams)
167 Gathers console arguments, or prompts for them.
168 CreateUser(UUID.Zero, principalId, firstName, lastName, password, email, model);
169 public UserAccount CreateUser(UUID scopeID, UUID principalID, string firstName, string lastName, string password, string email, string model = "")
170 Looks almost identical to the OpenSim/ApplicationPlugins/RemoteController/RemoteAdminPlugin.cs one above, but they add -
171 CreateDefaultAppearanceEntries(account.PrincipalID)
172
173
174
175account = new UserAccount(scopeID, UUID.Random(), firstName, lastName, email);
176 OpenSim/opensim-SC/OpenSim/Services/Interfaces/IUserAccountService.cs
177 public UserAccount(UUID scopeID, UUID principalID, string firstName, string lastName, string email)
178 Just holds the data in memory, in a dictionary I think.
179 OpenSim/opensim-SC/OpenSim/Services/UserAccountService/UserAccountService.cs
180 public bool StoreUserAccount(UserAccount data)
181 Stuffs the data into a new UserAccountData()
182 m_Database.Store(d)
183 As far as I can tell, just dumps this data into the UserAccounts table -
184 FirstName, LastName, PrincipleID, ScopeID, Email, Created, UserLevel, UserFlags, UserTitle
185 PrincipleID is their randomly generated with no thought to collisions UUID.
186 ScopeID is 00000000-0000-0000-0000-000000000000
187 Userlevel is 0 for most, -1 for Waki, determines if they can log on. Also higher for gods and things.
188 UserFlags, I think the only one is "64 god can login to this account using gods password.
189 UserTitle might default to "Local", or be configurable / and editable.
190 something something URL encoded "ServiceURLs" mumble
191 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
192 Though most are either NULL, empty, or -
193 HomeURI= GatekeeperURI= InventoryServerURI= AssetServerURI=
194 Doesn't metion "active", which is always equal to 1 I guess.
195
196
197
198success = authenticationService.SetPassword(account.PrincipalID, password)
199 OpenSim/Services/AuthenticationService/AuthenticationServiceBase.cs
200 stores password details in "auth" table -
201 UUID
202 passwordSalt = Util.Md5Hash(UUID.Random().ToString());
203 passwdHash = Util.Md5Hash(Util.Md5Hash(password) + ":" + passwordSalt);
204 accountType = "UserAccount";
205 webLoginKey = UUID.Zero.ToString();
206
207
208
209gridUserService.SetHome(account.PrincipalID.ToString(), home.RegionID, new Vector3(128, 128, 0), new Vector3(0, 1, 0));
210 OpenSim/Services/UserAccountService/GridUserService.cs
211 Stores in database table GridUser
212 HomeRegionID, HomePosition, HomeLookAt
213 The other fields in that table -
214 UserID, LastRegionID, LastPosition, LastLookAt, Online (true or false), Login (timestamp or 0), Logout (timestamp or 0).
215
216
217
218success = inventoryService.CreateUserInventory(account.PrincipalID);
219 OpenSim/Services/InventoryService/XInventoryService.cs
220 Create a bunch of folders in the users inventory, of specific types.
221 rootFolder = ConvertToOpenSim(CreateFolder(principalID, UUID.Zero, (int)FolderType.Root, InventoryFolderBase.ROOT_FOLDER_NAME));
222 XInventoryFolder[] sysFolders = GetSystemFolders(principalID, rootFolder.ID)
223 if (!Array.Exists(sysFolders, delegate(XInventoryFolder f) { if (f.type == (int)FolderType.Animation) return true; return false; }))
224 CreateFolder(principalID, rootFolder.ID, (int)FolderType.Animation, "Animations");
225 FolderType.BodyPart, "Body Parts"
226 XInventoryFolder folder = CreateFolder(principalID, rootFolder.ID, (int)FolderType.CallingCard, "Calling Cards");
227 folder = CreateFolder(principalID, folder.folderID, (int)FolderType.CallingCard, "Friends")
228 CreateFolder(principalID, folder.folderID, (int)FolderType.CallingCard, "All");
229 FolderType.Clothing, "Clothing"
230 FolderType.CurrentOutfit, "Current Outfit"
231 FolderType.Favorites, "Favorites"
232 FolderType.Gesture, "Gestures")
233 FolderType.Landmark, "Landmarks"
234 FolderType.LostAndFound, "Lost And Found"
235 FolderType.Notecard, "Notecards"
236 FolderType.Object, "Objects"
237 FolderType.Snapshot, "Photo Album"
238 FolderType.LSLText, "Scripts"
239 FolderType.Sound, "Sounds"
240 FolderType.Texture, "Textures"
241 FolderType.Trash, "Trash"
242
243 Stores in database inventoryFolders ????
244 folderName, type, version = 1, folderID = UUID.Random(), agentID = principalID, parentFolderID = parentID
245
246
247
248
249CreateDefaultAppearanceEntries(account.PrincipalID)
250 OpenSim/Services/UserAccountService/UserAccountService.cs
251 protected void CreateDefaultAppearanceEntries(UUID principalID)
252 Creates a bunch of "Default *" body parts and clothes, Ruth 1.0, links them in Inventories current outfit folder.
253 Creates a AvatarWearable[] and puts them all in it.
254 AvatarAppearance ap = new AvatarAppearance();
255 ap.SetWearable(i, wearables[i]);
256 m_AvatarService.SetAppearance(principalID, ap);
257
258
259
260
261
262UserAccounts table -
263 UserFlags 64 is "allow gods to log in as me"
264 0xf00 is membershipType, unles there's a title. Only sent to viewers I think.
265 32 is Minors for estate banning purposes.
266 4 is Anonymous for estate banning purposes.
267 1 is AllowPublish in profile, but userprofile has this as separate field.
268 2 is MaturePublish in profile, but userprofile has this as separate field.
269Presence table -
270 UserID varchar(255)
271 RegionID char(36)
272 SessionID char(36)
273 SecureSessionID char(36)
274 LastSeen timestamp
275tokens table (I think this is actually used for something) -
276 UUID char(36)
277 token varchar(255) current example looks like a UUID.
278 validity datetime
279userdata (empty, can't find any actual usage in the source code, part of profiles) -
280 UserId char(36) primary index
281 TagId varchar(64) primary index
282 DataKey varchar(255)
283 DataVal varchar(255)
284auth.webLoginKey seems to be some sort of passwordy type thing, though perhaps not actually hashed, rarely used, none of IG members have one.
285
286
287PLAN-
288. username
289. password
290. create login
291
292.check if it's a proper two word name
293.login -> check if it's an existing account, get their UUID.
294 create toke_n_munchie
295 write session record
296
297create -> new user
298 create new UUID
299 check if it's an existing UUID
300 dbCount(, "UserAccounts", "PrincipleID='new-UUID'")
301 loop until we get a new one
302 create toke_n_munchie
303 write session record
304
305
306 Create ->
307 (wait a few seconds before showing this page)
308. email
309. email again
310. password again
311. DoB
312. accept terms of service
313. claim to be an adult
314. confirm / cancel
315
316 New user
317 UserAccounts.FirstName = ???
318 UserAccounts.LastName = ???
319 UserAccounts.Email = ???
320 UserAccounts.Created = timestamp
321 UserAccounts.PrincipleID = randomly generate UUID, but check for collisions with other accounts.
322 It's a UNIQUE KEY.
323 UserAccounts.ScopeID = 00000000-0000-0000-0000-000000000000
324 UserAccounts.Userlevel = -200
325 UserAccounts.UserFlags = 64
326 UserAccounts.UserTitle = newbie
327 UserAccounts.ServiceURLs = ""
328 UserAccounts.active = 0
329
330 auth.UUID = UserAccounts.PrincipleID
331 It's a PRIMARY KEY.
332 auth.passwordSalt = Util.Md5Hash(UUID.Random().ToString())
333 auth.passwdHash = Util.Md5Hash(Util.Md5Hash(password) + ":" + passwordSalt)
334 auth.accountType = "UserAccount"
335 auth.webLoginKey (varchar(255)) = "00000000-0000-0000-0000-000000000000"
336
337 userdata.UserId = UserAccounts.PrincipleID
338 userdata.TagId = "account creation data"
339 It's a UNIQUE KEY
340 userdata.DataKey = "DoB"
341 userdata.DataVal = ???
342
343 userdata.UserId = UserAccounts.PrincipleID
344 userdata.TagId = "account creation data"
345 userdata.DataKey = "timezone"
346 userdata.DataVal = ???
347
348 userdata.UserId = UserAccounts.PrincipleID
349 userdata.TagId = "account creation data"
350 userdata.DataKey = "Terms of service"
351 userdata.DataVal = "True"
352
353 userdata.UserId = UserAccounts.PrincipleID
354 userdata.TagId = "account creation data"
355 userdata.DataKey = "claims to be an adult"
356 userdata.DataVal = "True"
357
358
359 Validated via email
360 (wait a few seconds before showing this page)
361 UserAccounts.Userlevel = -100
362 UserAccounts.UserTitle = validated
363
364
365 Vouched for
366 userdata.UserId = UserAccounts.PrincipleID
367 userdata.TagId = "vouches"
368 userdata.DataKey = UUID of voucher
369 userdata.DataVal = timestamp of vouching
370
371 UserAccounts.Userlevel = -50
372 UserAccounts.UserTitle = vouched for
373
374
375 Admin approved
376 GridUser.UserID = UserAccounts.PrincipleID
377 It's a PRIMARY KEY.
378 GridUser.HomeRegionID = ???
379 GridUser.HomePosition = ???
380 GridUser.HomeLookAt = ???
381 GridUser.LastRegionID = ???
382 GridUser.LastPosition = ???
383 GridUser.LastLookAt = ???
384 GridUser.Online = False
385 GridUser.Login = 0
386 GridUser.Logout = 0
387
388 UserAccounts.active = 1
389 UserAccounts.Userlevel = 1
390 UserAccounts.UserTitle = Member / Local / whatever
391
392 Load the default IAR.
393
394
395-------------------------------------------------------------------
396
397https://project-awesome.org/aleksandar-todorovic/awesome-c
398 A curated list of C good stuff.
399
400https://wolkykim.github.io/qdecoder/
401 CGI library made by the qlibc guy, does support FCGI.
402 Might be a wrapper around the fcgi_stdio stuff I'm already using?
403
404
405https://danielmiessler.com/study/http/
406 A Security-focused HTTP Primer
407 Nothing much new except to say this about the Referer header -
408 "should not be used to make security decisions as it is controlled by the client"
409 Though others tell us to do precisely that. lol
410
411
412-------------------------------------------------------------------
413
414apt install libmariadbclient-dev libapache2-mod-fcgid
415
416-------------------------------------------------------------------
417
418Merge it into OpenSim-SC.
419
420 Complication - I had already added the boxes + early sledjchisl.c to opensim-SC.
421 I may have to revert that lot, only a few minor commits, which are already part of the main boxes.
422 d9c772712e27c8e25fab0d17555a9bc11017a125
423 d4ea3e50173df1ad646bdb7dc802f5d320b7e511
424 10ed36b3452ce6373175112716b043047dc896a9
425 2f66c46e7ce18d60cd1f565880fb7762b3399ccb
426 34c5ee4c2a489a506e93d5b303fbc80b263747f0 is the commit that added it.
427 f9bfa831d1ccaa973c42042584510e1a724ddaef the one before it.
428 It's been pushed up to the sledjhamr.org repo.
429
430 git revert d9c772712e27c8e25fab0d17555a9bc11017a125 d4ea3e50173df1ad646bdb7dc802f5d320b7e511 10ed36b3452ce6373175112716b043047dc896a9 2f66c46e7ce18d60cd1f565880fb7762b3399ccb 34c5ee4c2a489a506e93d5b303fbc80b263747f0
431 git remote add -f boxes ~/MyLinux_3/TOYBOX/sledjchisl
432 git merge --allow-unrelated-histories boxes/master
433 git remote rm boxes
434
435
436https://medium.com/altcampus/how-to-merge-two-or-multiple-git-repositories-into-one-9f8a5209913f
437https://medium.com/@checko/merging-two-git-repositories-into-one-preserving-the-git-history-4e20d3fafa4e
438https://thoughts.t37.net/merging-2-different-git-repositories-without-losing-your-history-de7a06bba804
439https://blog.doismellburning.co.uk/merging-two-git-repositories/
440
441Other much more complicated variations.
442
443http://mbork.pl/2019-08-19_Transplanting_a_directory_to_another_Git_repository
444
445
446-------------------------------------------------------------------
447
448Install / update / upgrade.
449
450I could keep the version number around.
451 Include version numbers / branches of dependencies.
452 Update will grab any security updates for the installed version.
453 Upgrade will upgrade to a chosen later different version.
454 Downgrade will downgrade to a chosen earlier different version.
455
456Note that we are currently using the LuaJIT 2.1.0-beta3 branch of the
457main Luajit repo. Everything else is on their master branches.
458
459Bootstrap -
460 bootstrap.sh or bootstrap.bat
461
462 Build the LuaJIT that comes with our source. It "builds out-of-the
463 box on most systems" and has no dependencies, other than a C build system.
464
465 Or download a prebuilt LuaJIT from somewhere.
466
467 After toybox has been LuaJITized.
468
469 Build the LuaJIT that comes with our source. It "builds out-of-the
470 box on most systems" and has no dependencies, other than a C build system.
471
472 Similar should apply to toybox, though it's our LuaJITized version.
473 Will need a specific miniconfig for this that doesn't include sledjchisl.
474
475 Or download a prebuilt toybox+LuaJIT from a SledjHamr package repo.
476
477Install -
478 install.lua
479
480 Will need a pre flight check if the dependencies are installed.
481 It checks if the system is built and has source.
482 Build it all.
483 Do the usual copy stuff to a directory thing.
484 Run "sledjchisl -install" in that directory.
485 Which does the usual "check health of system and fix up problems" thing, then quits instead of keep running.
486 The health check should include making sure our database creds exist / work.
487
488Update / upgrade / downgrade
489 install.lua -update
490 install.lua -upgrade
491 install.lua -downgrade
492
493 Check if we are a binary only, or a source install.
494 wget new binaries / git pull new source
495 Toybox has a wget in pending, otherwise it only has ftpget.
496 Git is standalone outside of the system, but if you are
497 running from source, you likely have standard build tools
498 like git.
499
500
501Yeah I hate things that have their own packaging system, for needing to
502step outside the operating systems packaging system, and adding to the too
503long 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 @@
1I'm re-purposing this for SledjHamr https://sledjhamr.org/git/docs/index.html
2
3The general structure of SledjHamr is a bunch of servers talking to each
4other via Internet (or just local) connections. One of them is a web
5server for assets, world data, and inventory.
6
7Originally I didn't think using a web based world client was a good idea,
8however it might be better to have one, for reasons. Now I need a web
9management console that can do all the things the current tmux console
10can, including OpenSim console and commands. Plus account management for
11users. I can also use a web based Jabber / XMPP front end to chat, IM,
12and group chatter, which would run in the normal viewers web browser.
13This provides a doorway into putting SledjHamr stuff in existing viewers
14without needing them to support it. So a web based viewer now makes more
15sense, and also means we can get away with not needing a viewer at all.
16
17Toybox itself doesn't include a web server, and I don't think there is
18one on the roadmap. So we have to use an external web server, which was
19a design goal of SledjHamr in the first place, using existing mature
20HTTP infrastructure, coz that's already solved problems for a bunch of
21things that plague OS/SL to this day. Clear your cache! Pffft.
22
23So sledjchisl.c will be the "love world server", though initially it just
24drives OpenSim_SC in tmux via tmux commands to send keys and read output.
25Later it might run opensim_SC directly and use STDIN and STDOUT to do
26everything. It'll also provide the text management front end that runs
27in the left tmux panel of the first window, which is why it's based on
28boxes in the first place. Later still it can take over opensim_SC
29functions as I move them out of mono.
30
31We will need a text, web, and GUI version of this management front end.
32Hmmm, maybe don't need a GUI version, GUI users can just run a terminal.
33
34After much research, FastCGI / FCGI seems to be the most portable way of
35interfacing with existing web servers. FCGI protocol closes STDERR and
36STDOUT, and uses STDIN as it's two way communications channel to the web
37server, so our FCGI module can't be used as the text management front
38end. This is probably a good idea to keep them seperate anyway, for
39security, coz the web server is exposed to the world, the console isn't.
40
41Currently sledjchisl.c tests to see if it's running in tmux already, if
42it isn't it starts up tmux runs itself into this new tmux, then exits.
43So it could also test if it's running from FCGI, and switch to web mode,
44then it'll need to find the tmuxed instance to send commands to it.
45Either via nails connection, or sending tmux commands via shell.
46
47FCGI has methods of dealing with auth and templates. B-)
48
49So for now I think I'll have the text and web management front ends in
50sledjchisl.c, and the love world server as well. I can split them up
51later 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 @@
1/* fcgi_SC.h - Generic fcgi handler, coz the others all suck.
2 *
3 * Copyright 2020 David Seikel <onefang@sledjhamr.org>
4 */
5
6// I use camelCaseNames internally, instead of underscore_names as is preferred
7// in the rest of toybox. A small limit of 80 characters per source line infers
8// shorter names should be used. CamelCaseNames are shorter. Externally visible
9// stuff is underscore_names as usual. Plus, I'm used to camelCaseNames, my
10// fingers twitch that way.
11
12#include "toys.h"
13#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 @@
1/* fcgi_SC.h - Generic fcgi handler, coz the oters all suck.
2 *
3 * Copyright 2020 David Seikel <onefang@sledjhamr.org>
4 */
5
6enum fcgiEventType{
7 FSC_CSI,
8 FSC_KEYS,
9 FSC_MOUSE,
10 FSC_RAW
11};
12
13struct fcgiEvent {
14 enum fcgiEventType type; // The type of this event.
15 char *sequence; // Either a translated sequence, or raw bytes.
16 int isTranslated; // Whether or not sequence is translated.
17 int count; // Number of entries in params.
18 int *params; // For CSI events, the decoded parameters.
19};
20
21
22
23
24// From the spec.
25
26/*
27 * Listening socket file number
28 */
29#define FCGI_LISTENSOCK_FILENO 0
30
31typedef struct {
32 unsigned char version;
33 unsigned char type;
34 unsigned char requestIdB1;
35 unsigned char requestIdB0;
36 unsigned char contentLengthB1;
37 unsigned char contentLengthB0;
38 unsigned char paddingLength;
39 unsigned char reserved;
40} FCGI_Header;
41
42/*
43 * Number of bytes in a FCGI_Header. Future versions of the protocol
44 * will not reduce this number.
45 */
46#define FCGI_HEADER_LEN 8
47
48/*
49 * Value for version component of FCGI_Header
50 */
51#define FCGI_VERSION_1 1
52
53/*
54 * Values for type component of FCGI_Header
55 */
56#define FCGI_BEGIN_REQUEST 1
57#define FCGI_ABORT_REQUEST 2
58#define FCGI_END_REQUEST 3
59#define FCGI_PARAMS 4
60#define FCGI_STDIN 5
61#define FCGI_STDOUT 6
62#define FCGI_STDERR 7
63#define FCGI_DATA 8
64#define FCGI_GET_VALUES 9
65#define FCGI_GET_VALUES_RESULT 10
66#define FCGI_UNKNOWN_TYPE 11
67#define FCGI_MAXTYPE (FCGI_UNKNOWN_TYPE)
68
69/*
70 * Value for requestId component of FCGI_Header
71 */
72#define FCGI_NULL_REQUEST_ID 0
73
74typedef struct {
75 unsigned char roleB1;
76 unsigned char roleB0;
77 unsigned char flags;
78 unsigned char reserved[5];
79} FCGI_BeginRequestBody;
80
81typedef struct {
82 FCGI_Header header;
83 FCGI_BeginRequestBody body;
84} FCGI_BeginRequestRecord;
85
86/*
87 * Mask for flags component of FCGI_BeginRequestBody
88 */
89#define FCGI_KEEP_CONN 1
90
91/*
92 * Values for role component of FCGI_BeginRequestBody
93 */
94#define FCGI_RESPONDER 1
95#define FCGI_AUTHORIZER 2
96#define FCGI_FILTER 3
97
98typedef struct {
99 unsigned char appStatusB3;
100 unsigned char appStatusB2;
101 unsigned char appStatusB1;
102 unsigned char appStatusB0;
103 unsigned char protocolStatus;
104 unsigned char reserved[3];
105} FCGI_EndRequestBody;
106
107typedef struct {
108 FCGI_Header header;
109 FCGI_EndRequestBody body;
110} FCGI_EndRequestRecord;
111
112/*
113 * Values for protocolStatus component of FCGI_EndRequestBody
114 */
115#define FCGI_REQUEST_COMPLETE 0
116#define FCGI_CANT_MPX_CONN 1
117#define FCGI_OVERLOADED 2
118#define FCGI_UNKNOWN_ROLE 3
119
120/*
121 * Variable names for FCGI_GET_VALUES / FCGI_GET_VALUES_RESULT records
122 */
123#define FCGI_MAX_CONNS "FCGI_MAX_CONNS"
124#define FCGI_MAX_REQS "FCGI_MAX_REQS"
125#define FCGI_MPXS_CONNS "FCGI_MPXS_CONNS"
126
127typedef struct {
128 unsigned char type;
129 unsigned char reserved[7];
130} FCGI_UnknownTypeBody;
131
132typedef struct {
133 FCGI_Header header;
134 FCGI_UnknownTypeBody body;
135} FCGI_UnknownTypeRecord;
136
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 @@
1-- script.lua
2
3-- This works coz LuaJIT automatically loads the jit module.
4if type(jit) == 'table' then
5 io.write('script.lua is being run by ' .. jit.version .. ' under ' .. jit.os .. ' on a ' .. jit.arch .. '\n')
6else
7 io.write('script.lua is being run by Lua version ' .. _VERSION .. '\n')
8end
9
10-- Receives a table, returns the sum of its components.
11io.write("The table the script received has:\n");
12x = 0
13for i = 1, #foo do
14 print(i, foo[i])
15 x = x + foo[i]
16end
17io.write("Returning data back to C\n");
18return 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 @@
1/* sledjchisl.c - opensim-SC management system.
2 *
3 * Copyright 2020 David Seikel <sledjchisl@sledjhamr.org>
4 * Not in SUSv4. An entirely new invention, thus no web site either.
5
6USE_SLEDJCHISL(NEWTOY(sledjchisl, "m(mode):", TOYFLAG_USR|TOYFLAG_BIN))
7
8config SLEDJCHISL
9 bool "sledjchisl"
10 default y
11 help
12 usage: sledjchisl [-m|--mode mode]
13
14 opensim-SC management system.
15*/
16
17
18// TODO - figure out how to automate testing of this.
19// Being all interactive and involving external web servers / viewers makes it hard.
20
21// TODO - do all the setup on first run, and check if needed on each start up, to be self healing.
22
23// TODO - create any missing directories when we need them.
24
25// TODO - use a FHS compatible setup -
26// scRoot = / /usr/local /opt/opensim_SC
27// /etc/opensim_SC /usr/local/etc/opensim_SC /opt/opensim_SC/etc
28// /run/opensim_SC /run/opensim_SC /run/opensim_SC
29// /usr/bin /usr/local/bin /opt/opensim_SC/bin
30// The problem here is that OpenSim already uses opensim/bin for all of it's crap, where they mix it all together.
31// Don't want it in the path.
32// Should put the .dlls into ../lib..
33// Most of the rest is config files or common assets.
34// /usr/lib/opensim_SC /usr/local/lib/opensim_SC /opt/opensim_SC/lib
35// /var/backups/opensim_SC /var/local/backups/opensim_SC /opt/opensim_SC/var/backups
36// /var/cache/opensim_SC /var/local/cache/opensim_SC /opt/opensim_SC/var/cache
37// /var/lib/opensim_SC /var/local/lib/opensim_SC /opt/opensim_SC/var/lib
38// /var/log/opensim_SC /var/local/log/opensim_SC /opt/opensim_SC/var/log
39// /var/www/opensim_SC /var/local/www/opensim_SC /opt/opensim_SC/var/www
40
41// TODO - pepper could be entered on the console on startup if it's not defined, as a master password sort of thing.
42// I'd go as far as protecting the database credentials that way, but legacy OpenSim needs it unprotected.
43// Also keep in mind, people want autostart of their services without having to enter passwords on each boot.
44
45// TODO - once it is event driven, periodically run things like session clean ups, self healing, and the secure.sh thing.
46// And backups off course.
47
48#include <fcgi_config.h>
49#ifdef _WIN32
50#include <process.h>
51#else
52extern char **environ;
53#endif
54// Don't overide standard stdio stuff.
55#define NO_FCGI_DEFINES
56#include <fcgi_stdio.h>
57#undef NO_FCGI_DEFINES
58//#include "fcgiapp.h"
59
60#include <lua.h>
61#include <lualib.h>
62#include <lauxlib.h>
63#include <luajit.h>
64
65#include "lib/fcgi_SC.h"
66#include "lib/handlekeys.h"
67
68// Both my_config.h and fcgi_config.h define the same PACKAGE* variables, which we don't use anyway,
69// I deal with that by using a sed invokation when building fcgi2.
70
71// https://mariadb.com/kb/en/about-mariadb-connector-c/ Official docs.
72// http://dev.mysql.com/doc/refman/5.5/en/c-api-function-overview.html MySQL docs.
73// http://zetcode.com/db/mysqlc/ MySQL tutorial.
74#include <my_global.h>
75#include <mysql.h>
76
77// TODO - audit all the alloc()s and free()s involved in qlibc stuff.
78#include <qlibc.h>
79#include <extensions/qconfig.h>
80
81#include <openssl/evp.h>
82#include "openssl/hmac.h"
83#include <uuid/uuid.h>
84
85// Toybox's strend overrides another strend that causes MariaDB library to crash. Renaming it to tb_strend helps.
86#include "toys.h"
87
88
89GLOBALS(
90 char *mode;
91)
92
93#define TT this.sledjchisl
94
95#define FLAG_m 2
96
97
98
99
100// Duplicate some small amount of code from qlibc, coz / and = are not good choices, and the standard says we can pick those.
101/**
102 * Encode data using BASE64 algorithm.
103 *
104 * @param bin a pointer of input data.
105 * @param size the length of input data.
106 *
107 * @return a malloced string pointer of BASE64 encoded string in case of
108 * successful, otherwise returns NULL
109 *
110 * @code
111 * const char *text = "hello world";
112 *
113 * char *encstr = qB64_encode(text, strlen(text));
114 * if(encstr == NULL) return -1;
115 *
116 * printf("Original: %s\n", text);
117 * printf("Encoded : %s\n", encstr);
118 *
119 * size_t decsize = qB64_decode(encstr);
120 *
121 * printf("Decoded : %s (%zu bytes)\n", encstr, decsize);
122 * free(encstr);
123 *
124 * --[output]--
125 * Original: hello world
126 * Encoded: aGVsbG8gd29ybGQ=
127 * Decoded: hello world (11 bytes)
128 * @endcode
129 */
130char *qB64_encode(const void *bin, size_t size) {
131 const char B64CHARTBL[64] = {
132 'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P', // 00-0F
133 'Q','R','S','T','U','V','W','X','Y','Z','a','b','c','d','e','f', // 10-1F
134 'g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v', // 20-2F
135// 'w','x','y','z','0','1','2','3','4','5','6','7','8','9','+','/' // 30-3F
136 'w','x','y','z','0','1','2','3','4','5','6','7','8','9','+','_' // 30-3F
137 };
138
139 if (size == 0) {
140 return strdup("");
141 }
142
143 // malloc for encoded string
144 char *pszB64 = (char *) malloc(
145 4 * ((size / 3) + ((size % 3 == 0) ? 0 : 1)) + 1);
146 if (pszB64 == NULL) {
147 return NULL;
148 }
149
150 char *pszB64Pt = pszB64;
151 unsigned char *pBinPt, *pBinEnd = (unsigned char *) (bin + size - 1);
152 unsigned char szIn[3] = { 0, 0, 0 };
153 int nOffset;
154 for (pBinPt = (unsigned char *) bin, nOffset = 0; pBinPt <= pBinEnd;
155 pBinPt++, nOffset++) {
156 int nIdxOfThree = nOffset % 3;
157 szIn[nIdxOfThree] = *pBinPt;
158 if (nIdxOfThree < 2 && pBinPt < pBinEnd)
159 continue;
160
161 *pszB64Pt++ = B64CHARTBL[((szIn[0] & 0xFC) >> 2)];
162 *pszB64Pt++ = B64CHARTBL[(((szIn[0] & 0x03) << 4)
163 | ((szIn[1] & 0xF0) >> 4))];
164 *pszB64Pt++ =
165 (nIdxOfThree >= 1) ?
166 B64CHARTBL[(((szIn[1] & 0x0F) << 2)
167 | ((szIn[2] & 0xC0) >> 6))] :
168// '=';
169 '-';
170// *pszB64Pt++ = (nIdxOfThree >= 2) ? B64CHARTBL[(szIn[2] & 0x3F)] : '=';
171 *pszB64Pt++ = (nIdxOfThree >= 2) ? B64CHARTBL[(szIn[2] & 0x3F)] : '-';
172
173 memset((void *) szIn, 0, sizeof(szIn));
174 }
175 *pszB64Pt = '\0';
176
177 return pszB64;
178}
179
180/**
181 * Decode BASE64 encoded string.
182 *
183 * @param str a pointer of Base64 encoded string.
184 *
185 * @return the length of bytes stored in the str memory in case of successful,
186 * otherwise returns NULL
187 *
188 * @note
189 * This modify str directly. And the 'str' is always terminated by NULL
190 * character.
191 */
192size_t qB64_decode(char *str) {
193 const char B64MAPTBL[16 * 16] = {
194 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, // 00-0F
195 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, // 10-1F
196 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 62, 64, 64, 64, 63, // 20-2F
197 52, 53, 54, 55, 56, 57, 58, 59, 60, 45, 64, 64, 64, 64, 64, 64, // 30-3F
198 64, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, // 40-4F
199 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 64, 64, 64, 64, 64, // 50-5F
200 64, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, // 60-6F
201 41, 42, 43, 44, 45, 46, 95, 48, 49, 50, 51, 64, 64, 64, 64, 64, // 70-7F
202 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, // 80-8F
203 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, // 90-9F
204 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, // A0-AF
205 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, // B0-BF
206 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, // C0-CF
207 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, // D0-DF
208 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, // E0-EF
209 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64 // F0-FF
210 };
211
212 char *pEncPt, *pBinPt = str;
213 int nIdxOfFour = 0;
214 char cLastByte = 0;
215 for (pEncPt = str; *pEncPt != '\0'; pEncPt++) {
216 char cByte = B64MAPTBL[(unsigned char) (*pEncPt)];
217 if (cByte == 64)
218 continue;
219
220 if (nIdxOfFour == 0) {
221 nIdxOfFour++;
222 } else if (nIdxOfFour == 1) {
223 // 00876543 0021????
224 //*pBinPt++ = ( ((cLastByte << 2) & 0xFC) | ((cByte >> 4) & 0x03) );
225 *pBinPt++ = ((cLastByte << 2) | (cByte >> 4));
226 nIdxOfFour++;
227 } else if (nIdxOfFour == 2) {
228 // 00??8765 004321??
229 //*pBinPt++ = ( ((cLastByte << 4) & 0xF0) | ((cByte >> 2) & 0x0F) );
230 *pBinPt++ = ((cLastByte << 4) | (cByte >> 2));
231 nIdxOfFour++;
232 } else {
233 // 00????87 00654321
234 //*pBinPt++ = ( ((cLastByte << 6) & 0xC0) | (cByte & 0x3F) );
235 *pBinPt++ = ((cLastByte << 6) | cByte);
236 nIdxOfFour = 0;
237 }
238
239 cLastByte = cByte;
240 }
241 *pBinPt = '\0';
242
243 return (pBinPt - str);
244}
245
246
247
248
249// Duplicate some small amount of code from toys/pending/sh.c
250int runToy(char *argv[])
251{
252 int ret = 0;
253 struct toy_list *tl;
254 struct toy_context temp;
255 sigjmp_buf rebound;
256
257 if ((tl = toy_find(argv[0])) )//&& (tl->flags & (TOYFLAG_NOFORK|TOYFLAG_MAYFORK)))
258 {
259 // This fakes lots of what toybox_main() does.
260 memcpy(&temp, &toys, sizeof(struct toy_context));
261 memset(&toys, 0, sizeof(struct toy_context));
262
263 if (!sigsetjmp(rebound, 1))
264 {
265 toys.rebound = &rebound;
266 toy_init(tl, argv); // argv must be null terminated
267 tl->toy_main();
268 xflush(0);
269 }
270 ret = toys.exitval;
271 if (toys.optargs != toys.argv+1) free(toys.optargs);
272 if (toys.old_umask) umask(toys.old_umask);
273 memcpy(&toys, &temp, sizeof(struct toy_context));
274 }
275
276 return ret;
277}
278
279
280#undef FALSE
281#undef TRUE
282#ifndef FALSE
283// NEVER change this
284typedef enum
285{
286 FALSE = 0,
287 TRUE = 1
288} boolean;
289#endif
290
291
292
293// Silly "man getrandom" is bullshitting.
294// Note - this is Linux specific, it's calling a Linux kernel function.
295// Remove this when we have a real getrandom(), and replace it with -
296// #include <sys/random.h>
297#include <sys/syscall.h>
298#include <linux/random.h>
299int getrandom(void *b, size_t l, unsigned int f)
300{
301 return (int) syscall(SYS_getrandom, b, l, f);
302}
303
304
305
306typedef struct _gridStats gridStats;
307struct _gridStats
308{
309 float next;
310 struct timeval last;
311 qhashtbl_t *stats;
312};
313
314
315typedef struct _HTMLfile HTMLfile;
316struct _HTMLfile
317{
318 struct timespec last;
319 qlist_t *fragments;
320};
321qhashtbl_t *HTMLfileCache = NULL;
322
323
324typedef struct _reqData reqData;
325
326
327typedef int (*fieldValidFunc) (reqData *Rd, qhashtbl_t *data);
328typedef struct _validFunc validFunc;
329struct _validFunc
330{
331 char *name;
332 fieldValidFunc func;
333};
334qlisttbl_t *fieldValidFuncs = NULL;
335static void newValidFunc(char *name, fieldValidFunc func)
336{
337 validFunc *vf = xmalloc(sizeof(validFunc));
338 vf->name = name; vf->func = func;
339 fieldValidFuncs->put(fieldValidFuncs, vf->name, vf, sizeof(validFunc));
340}
341
342typedef void *(*pageFunction) (char *file, reqData *Rd, HTMLfile *thisFile);
343typedef struct _dynPage dynPage;
344struct _dynPage
345{
346 char *name;
347 pageFunction func;
348};
349qhashtbl_t *dynPages;
350static void newDynPage(char *name, pageFunction func)
351{
352 dynPage *dp = xmalloc(sizeof(dynPage));
353 dp->name = name; dp->func = func;
354 dynPages->put(dynPages, dp->name, dp, sizeof(dynPage));
355}
356
357typedef void *(*pageBuildFunction) (reqData *Rd, char *message);
358typedef struct _buildPage buildPage;
359struct _buildPage
360{
361 char *name;
362 pageBuildFunction func, eFunc;
363};
364qhashtbl_t *buildPages;
365static void newBuildPage(char *name, pageBuildFunction func, pageBuildFunction eFunc)
366{
367 buildPage *bp = xmalloc(sizeof(buildPage));
368 bp->name = name; bp->func = func; bp->eFunc = eFunc;
369 buildPages->put(buildPages, bp->name, bp, sizeof(buildPage));
370}
371
372
373/* TODO - there should be some precedence for values overriding values here.
374 Nothing official?
375 https://www.w3.org/standards/webarch/protocols
376 "This intro text is boilerplate for the beta release of w3.org." Fucking useless. Pffft
377 https://www.w3.org/Protocols/
378 Still nothing official, though the ENV / HEADER stuff tends to be about the protocol things, and cookies / body / queries are about the data things.
379
380Also including values from the database.
381
382URL query Values actually provided by the user in the FORM, and other things.
383POST body Values actually provided by the user in the FORM.
384cookies
385 https://stackoverflow.com/questions/4056306/how-to-handle-multiple-cookies-with-the-same-name
386
387headers includes HTTP_COOKIE and QUERY_STRING
388env includes headers and HTTP_COOKIE and QUERY_STRING
389
390database Since all of the above are for updating the database anyway, this goes on the bottom, overridden by all.
391 Though be wary of security stuff.
392
393We don't actually get the headers directly, it's all sent via the env.
394
395http://docs.gantry.org/gantry4/advanced/setby
396 Says that query overrides cookies, but that might be just for their platform.
397
398https://framework.zend.com/manual/1.11/en/zend.controller.request.html
399 Says - "1. GET, 2. POST, 3. COOKIE, 4. SERVER, 5. ENV."
400
401
402Sending cookie headers is a special case, multiples can be sent, otherwise headers are singletons, only send one for each name.
403
404local storage? Would be client side Javascript thing not usually sent back to server.
405*/
406
407#define HMACSIZE EVP_MAX_MD_SIZE * 2
408#define HMACSIZE64 88
409typedef struct _sesh sesh;
410struct _sesh
411{
412 char salt[256 + 1], seshID[256 + 1],
413 sesh[256 + 16 + 10 + 1], munchie[HMACSIZE + 16 + 10 + 1], toke_n_munchie[HMACSIZE + 1], hashish[HMACSIZE + 1],
414 leaf[HMACSIZE64 + 6 + 1];
415 struct timespec timeStamp[2];
416 boolean isLinky;
417};
418
419struct _reqData
420{
421 lua_State *L;
422 qhashtbl_t *configs, *queries, *body, *cookies, *headers, *stuff, *database, *Rcookies, *Rheaders;
423 char *Scheme, *Host, *Method, *Script, *RUri, *doit;
424 sesh shs, *lnk;
425 MYSQL *db;
426 gridStats *stats;
427 qlist_t *errors, *messages;
428 qgrow_t *reply;
429 pageBuildFunction func;
430 struct timespec then;
431 boolean chillOut, vegOut;
432};
433
434static void showSesh(qgrow_t *reply, sesh *shs)
435{
436 if (shs->isLinky)
437 reply->addstrf(reply, "Linky:<br>\n<pre>\n");
438 else
439 reply->addstrf(reply, "Session:<br>\n<pre>\n");
440
441 reply->addstrf(reply, " &nbsp; salt = %s\n", shs->salt);
442 reply->addstrf(reply, " &nbsp; seshID = %s\n", shs->seshID);
443 reply->addstrf(reply, " &nbsp; timeStamp = %ld.%ld\n", shs->timeStamp[1].tv_sec, shs->timeStamp[1].tv_nsec);
444 reply->addstrf(reply, " &nbsp; sesh = %s\n", shs->sesh);
445 reply->addstrf(reply, " &nbsp; munchie = %s\n", shs->munchie);
446 reply->addstrf(reply, " &nbsp; toke_n_munchie = %s\n", shs->toke_n_munchie);
447 reply->addstrf(reply, " &nbsp; hashish = %s\n", shs->hashish);
448 reply->addstrf(reply, " &nbsp; leaf = %s\n", shs->leaf);
449 reply->addstr(reply, "</pre>\n");
450}
451
452
453char toybuf[4096];
454boolean isTmux = 0;
455boolean isWeb = 0;
456char *pwd = "";
457char *scRoot = "/opt/opensim_SC";
458char *scUser = "opensimsc";
459char *Tconsole = "SledjChisl";
460char *Tsocket = "caches/opensim-tmux.socket";
461char *Ttab = "SC";
462char *Tcmd = "tmux -S";
463char *webRoot = "/opt/opensim_SC/web";
464char *URL = "fcgi-bin/sledjchisl.fcgi";
465int seshTimeOut = 30 * 60;
466int idleTimeOut = 24 * 60 * 60;
467int newbieTimeOut = 30;
468float loadAverageInc = 0.5;
469int simTimeOut = 45;
470qhashtbl_t *mimeTypes;
471
472
473// 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.
474// Now that we are using spawn-fcgi, all the logs are going to STDOUT, which we can capture and write to a file.
475// A better idea, when we spawn tmux or spawn-fcgi, capture STDERR, full log everything to that, filtered log to the tmux console (STDOUT).
476// Then we can use STDOUT / STDIN to run the console stuff.
477
478// https://stackoverflow.com/questions/4842424/list-of-ansi-color-escape-sequences
479char *logTypes[] =
480{
481 "91;1;4", "CRITICAL", // red underlined
482 "31", "ERROR", // dark red
483 "93", "WARNING", // yellow
484 "36", "TIMEOUT", // cyan
485 "97;40", "INFO", // white
486 "90", "DEBUG", // grey
487};
488
489#define DATE_TIME_LEN 42
490void logMe(int v, char *format, ...)
491{
492 va_list va, va2;
493 int len;
494 char *ret;
495 struct timeval tv;
496 time_t curtime;
497 char date[DATE_TIME_LEN];
498
499 va_start(va, format);
500 va_copy(va2, va);
501 // How long is it?
502 len = vsnprintf(0, 0, format, va);
503 len++;
504 va_end(va);
505 // Allocate and do the sprintf()
506 ret = xmalloc(len);
507 vsnprintf(ret, len, format, va2);
508 va_end(va2);
509
510 gettimeofday(&tv, NULL);
511 curtime = tv.tv_sec;
512 strftime(date, DATE_TIME_LEN, "(%Z %z) %F %T", localtime(&curtime));
513
514 v *= 2;
515 fprintf(stderr, "%s.%.6ld \e[%sm%-8s sledjchisl: %s\e[0m\n", date, tv.tv_usec, logTypes[v], logTypes[v + 1], ret);
516 free(ret);
517}
518#define C(...) logMe(0, __VA_ARGS__)
519#define E(...) logMe(1, __VA_ARGS__)
520#define W(...) logMe(2, __VA_ARGS__)
521#define T(...) logMe(3, __VA_ARGS__)
522#define I(...) logMe(4, __VA_ARGS__)
523#define D(...) logMe(5, __VA_ARGS__)
524
525
526static void addStrL(qlist_t *list, char *s)
527{
528 list->addlast(list, s, strlen(s) + 1);
529}
530
531static char *getStrH(qhashtbl_t *hash, char *key)
532{
533 char *ret = "", *t;
534
535 t = hash->getstr(hash, key, false);
536 if (NULL != t)
537 ret = t;
538 return ret;
539}
540
541
542char *myHMAC(char *in, boolean b64)
543{
544 EVP_MD_CTX *mdctx = EVP_MD_CTX_create(); // Gets renamed to EVP_MD_CTX_new() in later versions.
545 unsigned char md_value[EVP_MAX_MD_SIZE];
546 unsigned int md_len;
547
548 EVP_DigestInit_ex(mdctx, EVP_sha512(), NULL); // EVP_sha3_512() isn't available until later versions.
549 EVP_DigestUpdate(mdctx, in, strlen(in));
550 EVP_DigestFinal_ex(mdctx, md_value, &md_len);
551 EVP_MD_CTX_destroy(mdctx); // Gets renamed to EVP_MD_CTX_free() in later versions.
552
553 if (b64)
554 return qB64_encode(md_value, md_len);
555 else
556 return qhex_encode(md_value, md_len);
557}
558
559char *myHMACkey(char *key, char *in, boolean b64)
560{
561 unsigned char md_value[EVP_MAX_MD_SIZE];
562 unsigned int md_len;
563 unsigned char* digest = HMAC(EVP_sha512(), key, strlen(key), (unsigned char *) in, strlen(in), md_value, &md_len);
564
565 if (b64)
566 return qB64_encode(md_value, md_len);
567 else
568 return qhex_encode(md_value, md_len);
569}
570
571
572// In Lua 5.0 reference manual is a table traversal example at page 29.
573void PrintTable(lua_State *L)
574{
575 lua_pushnil(L);
576
577 while (lua_next(L, -2) != 0)
578 {
579 // Numbers can convert to strings, so check for numbers before checking for strings.
580 if (lua_isnumber(L, -1))
581 printf("%s = %f\n", lua_tostring(L, -2), lua_tonumber(L, -1));
582 else if (lua_isstring(L, -1))
583 printf("%s = '%s'\n", lua_tostring(L, -2), lua_tostring(L, -1));
584 else if (lua_istable(L, -1))
585 PrintTable(L);
586 lua_pop(L, 1);
587 }
588}
589
590
591int sendTmuxKeys(char *dest, char *keys)
592{
593 int ret = 0, i;
594 char *c = xmprintf("%s %s/%s send-keys -t %s:%s '%s'", Tcmd, scRoot, Tsocket, Tconsole, dest, keys);
595
596 i = system(c);
597 if (!WIFEXITED(i))
598 E("tmux send-keys command failed!");
599 free(c);
600 return ret;
601}
602
603int sendTmuxCmd(char *dest, char *cmd)
604{
605 int ret = 0, i;
606 char *c = xmprintf("%s %s/%s send-keys -t %s:'%s' '%s' Enter", Tcmd, scRoot, Tsocket, Tconsole, dest, cmd);
607
608 i = system(c);
609 if (!WIFEXITED(i))
610 E("tmux send-keys command failed!");
611 free(c);
612 return ret;
613}
614
615void waitTmuxText(char *dest, char *text)
616{
617 int i;
618 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);
619
620 D("Waiting for '%s'.", text);
621 do
622 {
623 i = system(c);
624 if (!WIFEXITED(i))
625 {
626 E("tmux capture-pane command failed!");
627 break;
628 }
629 else if (0 == WEXITSTATUS(i))
630 break;
631 } while (1);
632
633 free(c);
634}
635
636float waitLoadAverage(float la, float extra, int timeout)
637{
638 struct sysinfo info;
639 struct timespec timeOut;
640 float l;
641 int to = timeout;
642
643 T("Sleeping until load average is below %.02f (%.02f + %.02f) or for %d seconds.", la + extra, la, extra, timeout);
644 clock_gettime(CLOCK_MONOTONIC, &timeOut);
645 to += timeOut.tv_sec;
646
647 do
648 {
649 msleep(5000);
650 sysinfo(&info);
651 l = info.loads[0]/65536.0;
652 clock_gettime(CLOCK_MONOTONIC, &timeOut);
653 timeout -= 5;
654 T("Tick, load average is %.02f, countdown %d seconds.", l, timeout);
655 } while (((la + extra) < l) && (timeOut.tv_sec < to));
656
657 return l;
658}
659
660
661// Rob forget to do this, but at least he didn't declare it static.
662struct dirtree *dirtree_handle_callback(struct dirtree *new, int (*callback)(struct dirtree *node));
663
664typedef struct _simList simList;
665struct _simList
666{
667 int len, num;
668 char **sims;
669};
670
671static int filterSims(struct dirtree *node)
672{
673 if (!node->parent) return DIRTREE_RECURSE | DIRTREE_SHUTUP;
674 if ((strncmp(node->name, "sim", 3) == 0) && ((strcmp(node->name, "sim_skeleton") != 0)))
675 {
676 simList *list = (simList *) node->parent->extra;
677
678 if ((list->num + 1) > list->len)
679 {
680 list->len = list->len + 1;
681 list->sims = xrealloc(list->sims, list->len * sizeof(char *));
682 }
683 list->sims[list->num] = xstrdup(node->name);
684 list->num++;
685 }
686 return 0;
687}
688
689simList *getSims()
690{
691 simList *sims = xmalloc(sizeof(simList));
692 memset(sims, 0, sizeof(simList));
693 char *path = xmprintf("%s/config", scRoot);
694 struct dirtree *new = dirtree_add_node(0, path, 0);
695 new->extra = (long) sims;
696 dirtree_handle_callback(new, filterSims);
697 qsort(sims->sims, sims->num, sizeof(char *), qstrcmp);
698 free(path);
699 return sims;
700}
701
702
703static int filterInis(struct dirtree *node)
704{
705 if (!node->parent) return DIRTREE_RECURSE | DIRTREE_SHUTUP;
706 int l = strlen(node->name);
707 if (strncmp(&(node->name[l - 4]), ".ini", 4) == 0)
708 {
709 strcpy((char *) node->parent->extra, xstrdup(node->name));
710 return DIRTREE_ABORT;
711 }
712 return 0;
713}
714
715char *getSimName(char *sim)
716{
717 char *ret = NULL;
718 char *c = xmprintf("%s/config/%s", scRoot, sim);
719 struct dirtree *new = dirtree_add_node(0, c, 0);
720
721 free(c);
722 c = xzalloc(1024);
723 new->extra = (long) c;
724 dirtree_handle_callback(new, filterInis);
725 if ('\0' != c[0])
726 {
727 char *temp = NULL;
728 regex_t pat;
729 regmatch_t m[2];
730 long len;
731 int fd;
732
733 temp = xmprintf("%s/config/%s/%s", scRoot, sim, c);
734 fd = xopenro(temp);
735 free(temp);
736 xregcomp(&pat, "RegionName = \"(.+)\"", REG_EXTENDED);
737 do
738 {
739 // TODO - get_line() is slow, and wont help much with DOS and Mac line endings.
740 temp = get_line(fd);
741 if (temp)
742 {
743 if (!regexec(&pat, temp, 2, m, 0))
744 {
745 // Return first parenthesized subexpression as string.
746 if (pat.re_nsub > 0)
747 {
748 ret = xmprintf("%.*s", (int) (m[1].rm_eo - m[1].rm_so), temp + m[1].rm_so);
749 break;
750 }
751 }
752 }
753 } while (temp);
754 xclose(fd);
755 }
756 free(c);
757 return ret;
758}
759
760
761// Expects either "simXX" or "ROBUST".
762// TODO - ROBUST isn't creating it's pid file, bug in ROBUST, it has config for it.
763int checkSimIsRunning(char *sim)
764{
765 int ret = 0;
766 struct stat st;
767
768 // Check if it's running.
769 char *path = xmprintf("%s/caches/%s.pid", scRoot, sim);
770 if (0 == stat(path, &st))
771 {
772 int fd, i;
773 char *pid = NULL;
774
775 // Double check if it's REALLY running.
776 if ((fd = xopenro(path)) == -1)
777 perror_msg("xopenro(%s)", path);
778 else
779 {
780 pid = get_line(fd);
781 if (NULL == pid)
782 perror_msg("get_line(%s)", path);
783 else
784 {
785 xclose(fd);
786
787// I'd rather re-use the toysh command running stuff, since ps is a toy, but that's private.
788// TODO - switch to toybox ps and rm.
789 free(path);
790 path = xmprintf("ps -p %s --no-headers -o comm", pid);
791 i = system(path);
792 if (WIFEXITED(i))
793 {
794 if (0 != WEXITSTATUS(i)) // No such pid.
795 {
796 free(path);
797 path = xmprintf("rm -f %s/caches/%s.pid", scRoot, sim);
798 D("%s", path);
799 i = system(path);
800 }
801 }
802 }
803 }
804 }
805
806 // Now check if it's really really running. lol
807 free(path);
808 path = xmprintf("%s/caches/%s.pid", scRoot, sim);
809 if (0 == stat(path, &st))
810 ret = 1;
811
812 free(path);
813 return ret;
814}
815
816
817static void PrintEnv(qgrow_t *reply, char *label, char **envp)
818{
819 reply->addstrf(reply, "%s:<br>\n<pre>\n", label);
820 for ( ; *envp != NULL; envp++)
821 reply->addstrf(reply, "%s\n", *envp);
822 reply->addstr(reply, "</pre>\n");
823}
824
825static void printEnv(char **envp)
826{
827 for ( ; *envp != NULL; envp++)
828 D("%s", *envp);
829}
830
831
832typedef struct _rowData rowData;
833struct _rowData
834{
835 char **fieldNames;
836 qlist_t *rows;
837};
838
839static void dumpHash(qhashtbl_t *tbl)
840{
841 qhashtbl_obj_t obj;
842
843 memset((void*)&obj, 0, sizeof(obj));
844 tbl->lock(tbl);
845 while(tbl->getnext(tbl, &obj, true) == true)
846 D("%s = %s", obj.name, (char *) obj.data);
847 tbl->unlock(tbl);
848}
849
850static void dumpArray(int d, char **ar)
851{
852 int i = 0;
853
854 while (ar[i] != NULL)
855 {
856 D("%d %d %s", d, i, ar[i]);
857 i++;
858 }
859}
860
861
862/* How to deal with prepared SQL statements.
863http://karlssonondatabases.blogspot.com/2010/07/prepared-statements-are-they-useful-or.html
864https://blog.cotten.io/a-taste-of-mysql-in-c-87c5de84a31d?gi=ab3dd1425b29
865https://raspberry-projects.com/pi/programming-in-c/databases-programming-in-c/mysql/accessing-the-database
866
867IG and CG now both have sims connected to other grids, so some sort of
868multi database solution would be good, then we can run the grid and the
869external sims all in one.
870
871Not sure if this'll work with Count(*).
872
873---------------------------------------------
874
875The complicated bit is the binds.
876
877You are binding field values to C memory locations.
878The parameters and returned fields need binds.
879Mostly seems to be the value parts of the SQL statements.
880
881I suspect most will be of the form -
882 ... WHERE x=? and foo=?
883 INSERT INTO table VALUES (?,?,?)
884 UPDATE table SET x=?, foo=? WHERE id=?
885
886 A multi table update -
887 UPDATE items,month SET items.price=month.price WHERE items.id=month.id;
888*/
889
890typedef struct _dbField dbField;
891struct _dbField
892{
893 char *name;
894 enum enum_field_types type;
895 unsigned long length;
896 unsigned int flags;
897 unsigned int decimals;
898};
899
900qlisttbl_t *dbGetFields(MYSQL *db, char *table)
901{
902 static qhashtbl_t *tables = NULL;
903 if (NULL == tables) tables = qhashtbl(0, 0);
904 qlisttbl_t *ret = tables->get(tables, table, NULL, false);
905
906 if (NULL == ret)
907 {
908 // 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.
909 // Chicken, meet egg, sorry you had to cross the road for this.
910 char *sql = xmprintf("SELECT * FROM %s LIMIT 0", table);
911
912D("Getting field metadata for %s", table);
913 if (mysql_query(db, sql))
914 E("Query failed: %s\n%s", mysql_error(db), sql);
915 else
916 {
917 MYSQL_RES *res = mysql_store_result(db);
918
919 if (!res)
920 E("Couldn't get results set from %s\n %s", mysql_error(db), sql);
921 else
922 {
923 MYSQL_FIELD *fields = mysql_fetch_fields(res);
924
925 if (!fields)
926 E("Failed fetching fields: %s", mysql_error(db));
927 else
928 {
929 unsigned int i, num_fields = mysql_num_fields(res);
930
931 ret = qlisttbl(QLISTTBL_UNIQUE | QLISTTBL_LOOKUPFORWARD);
932 for (i = 0; i < num_fields; i++)
933 {
934 dbField *fld = xmalloc(sizeof(dbField));
935 fld->name = xstrdup(fields[i].name);
936 fld->type = fields[i].type;
937 fld->length = fields[i].length;
938 fld->flags = fields[i].flags;
939 fld->decimals = fields[i].decimals;
940 ret->put(ret, fld->name, fld, sizeof(*fld));
941 }
942 tables->put(tables, table, ret, sizeof(*ret));
943 }
944 mysql_free_result(res);
945 }
946 }
947 free(sql);
948 }
949
950 return ret;
951}
952
953void dbFreeFields(qlisttbl_t *flds)
954{
955 qlisttbl_obj_t obj;
956 memset((void *) &obj, 0, sizeof(obj));
957 flds->lock(flds);
958 while(flds->getnext(flds, &obj, NULL, false) == true)
959 {
960 dbField *fld = (dbField *) obj.data;
961 free(fld->name);
962 }
963 flds->unlock(flds);
964 flds->free(flds);
965}
966
967typedef struct _dbRequest dbRequest;
968struct _dbRequest
969{
970 MYSQL *db;
971 char *table, *join, *where, *order, *sql;
972 qlisttbl_t *flds;
973 int inCount, outCount, rowCount;
974 char **inParams, **outParams;
975 MYSQL_STMT *prep; // NOTE - executing it stores state in this.
976 MYSQL_BIND *inBind, *outBind;
977 rowData *rows;
978 my_ulonglong count;
979 boolean freeOutParams;
980};
981
982void dbDoSomething(dbRequest *req, boolean count, ...)
983{
984 va_list ap;
985 struct timespec now, then;
986 int i, j;
987 MYSQL_RES *prepare_meta_result = NULL;
988
989 if (-1 == clock_gettime(CLOCK_REALTIME, &then))
990 perror_msg("Unable to get the time.");
991
992 va_start(ap, count);
993
994 if (NULL == req->prep)
995 {
996 req->flds = dbGetFields(req->db, req->table);
997 if (NULL == req->flds)
998 {
999 E("Unknown fields for table %s.", req->table);
1000 goto end;
1001 }
1002
1003 char *select = xmprintf("");
1004 i = 0;
1005 while (req->outParams[i] != NULL)
1006 {
1007 char *t = xmprintf("%s,%s", select, req->outParams[i]);
1008 free(select);
1009 select = t;
1010 i++;
1011 }
1012 if (0 == i)
1013 {
1014 if (count)
1015 select = xmprintf(",Count(*)");
1016 else
1017 select = xmprintf(",*");
1018 }
1019
1020 if (NULL == req->join)
1021 req->join = "";
1022
1023 if (req->where)
1024 req->sql = xmprintf("SELECT %s FROM %s %s WHERE %s", &select[1], req->table, req->join, req->where);
1025 else
1026 req->sql = xmprintf("SELECT %s FROM %s", &select[1], req->table, req->join);
1027 free(select);
1028 if (req->order)
1029 {
1030 char *t = xmprintf("%s ORDER BY %s", req->sql, req->order);
1031
1032 free(req->sql);
1033 req->sql = t;
1034 }
1035
1036D("New SQL statement - %s", req->sql);
1037 // prepare statement with the other fields
1038 req->prep = mysql_stmt_init(req->db);
1039 if (NULL == req->prep)
1040 {
1041 E("Statement prepare init failed: %s\n", mysql_stmt_error(req->prep));
1042 goto end;
1043 }
1044 if (mysql_stmt_prepare(req->prep, req->sql, strlen(req->sql)))
1045 {
1046 E("Statement prepare failed: %s\n", mysql_stmt_error(req->prep));
1047 goto end;
1048 }
1049
1050 // setup the bind stuff for any "?" parameters in the SQL.
1051 req->inCount = mysql_stmt_param_count(req->prep);
1052 i = 0;
1053 while (req->inParams[i] != NULL)
1054 i++;
1055 if (i != req->inCount)
1056 {
1057 E("In parameters count don't match %d != %d for - %s", i, req->inCount, req->sql);
1058 goto freeIt;
1059 }
1060 req->inBind = xzalloc(i * sizeof(MYSQL_BIND));
1061 for (i = 0; i < req->inCount; i++)
1062 {
1063 dbField *fld = req->flds->get(req->flds, req->inParams[i], NULL, false);
1064
1065 if (NULL == fld)
1066 {
1067 E("Unknown input field %d %s.%s for - %s", i, req->table, req->inParams[i], req->sql);
1068 goto freeIt;
1069 }
1070 else
1071 {
1072 // https://blog.cotten.io/a-taste-of-mysql-in-c-87c5de84a31d?gi=ab3dd1425b29
1073 // For some gotchas about all of this binding bit.
1074 req->inBind[i].buffer_type = fld->type;
1075 req->inBind[i].buffer = xzalloc(fld->length) + 1; // Note the + 1 is for string types, and a waste for the rest.
1076 req->inBind[i].buffer_length = fld->length;
1077 switch(fld->type)
1078 {
1079 case MYSQL_TYPE_TINY:
1080 {
1081 break;
1082 }
1083
1084 case MYSQL_TYPE_SHORT:
1085 {
1086 req->inBind[i].is_unsigned = FALSE;
1087 break;
1088 }
1089
1090 case MYSQL_TYPE_INT24:
1091 {
1092 req->inBind[i].is_unsigned = FALSE;
1093 break;
1094 }
1095
1096 case MYSQL_TYPE_LONG:
1097 {
1098 req->inBind[i].is_unsigned = FALSE;
1099 break;
1100 }
1101
1102 case MYSQL_TYPE_LONGLONG:
1103 {
1104 req->inBind[i].is_unsigned = FALSE;
1105 break;
1106 }
1107
1108 case MYSQL_TYPE_FLOAT:
1109 {
1110 break;
1111 }
1112
1113 case MYSQL_TYPE_DOUBLE:
1114 {
1115 break;
1116 }
1117
1118 case MYSQL_TYPE_NEWDECIMAL:
1119 {
1120 break;
1121 }
1122
1123 case MYSQL_TYPE_TIME:
1124 case MYSQL_TYPE_DATE:
1125 case MYSQL_TYPE_DATETIME:
1126 case MYSQL_TYPE_TIMESTAMP:
1127 {
1128 break;
1129 }
1130
1131 case MYSQL_TYPE_STRING:
1132 case MYSQL_TYPE_VAR_STRING:
1133 {
1134 req->inBind[i].is_null = xzalloc(sizeof(my_bool));
1135 req->inBind[i].length = xzalloc(sizeof(unsigned long));
1136 break;
1137 }
1138
1139 case MYSQL_TYPE_TINY_BLOB:
1140 case MYSQL_TYPE_BLOB:
1141 case MYSQL_TYPE_MEDIUM_BLOB:
1142 case MYSQL_TYPE_LONG_BLOB:
1143 {
1144 req->inBind[i].is_null = xzalloc(sizeof(my_bool));
1145 break;
1146 }
1147
1148 case MYSQL_TYPE_BIT:
1149 {
1150 req->inBind[i].is_null = xzalloc(sizeof(my_bool));
1151 break;
1152 }
1153
1154 case MYSQL_TYPE_NULL:
1155 {
1156 break;
1157 }
1158 }
1159 }
1160 }
1161
1162// TODO - if this is not a count, setup result bind paramateres, may be needed for counts as well.
1163 prepare_meta_result = mysql_stmt_result_metadata(req->prep);
1164 if (!prepare_meta_result)
1165 {
1166 D(" mysql_stmt_result_metadata(), returned no meta information - %s\n", mysql_stmt_error(req->prep));
1167 goto freeIt;
1168 }
1169
1170 if (count)
1171 {
1172I("count!!!!!!!!!!!!!!!!");
1173 }
1174 else
1175 {
1176 req->outCount = mysql_num_fields(prepare_meta_result);
1177 i = 0;
1178 while (req->outParams[i] != NULL)
1179 i++;
1180 if (0 == i) // Passing in {NULL} as req->outParams means "return all of them".
1181 {
1182 req->outParams = xzalloc((req->outCount + 1) * sizeof(char *));
1183 req->freeOutParams = TRUE;
1184 qlisttbl_obj_t obj;
1185 memset((void*)&obj, 0, sizeof(obj));
1186 req->flds->lock(req->flds);
1187 while (req->flds->getnext(req->flds, &obj, NULL, false) == true)
1188 {
1189 dbField *fld = (dbField *) obj.data;
1190 req->outParams[i] = fld->name;
1191 i++;
1192 }
1193 req->outParams[i] = NULL;
1194 req->flds->unlock(req->flds);
1195 }
1196 if (i != req->outCount)
1197 {
1198 E("Out parameters count doesn't match %d != %d foqr - %s", i, req->outCount, req->sql);
1199 goto freeIt;
1200 }
1201 req->outBind = xzalloc(i * sizeof(MYSQL_BIND));
1202 for (i = 0; i < req->outCount; i++)
1203 {
1204 dbField *fld = req->flds->get(req->flds, req->outParams[i], NULL, false);
1205
1206 if (NULL == fld)
1207 {
1208 E("Unknown output field %d %s.%s foqr - %s", i, req->table, req->outParams[i], req->sql);
1209 goto freeIt;
1210 }
1211 else
1212 {
1213 // https://blog.cotten.io/a-taste-of-mysql-in-c-87c5de84a31d?gi=ab3dd1425b29
1214 // For some gotchas about all of this binding bit.
1215 req->outBind[i].buffer_type = fld->type;
1216 req->outBind[i].buffer = xzalloc(fld->length + 1); // Note the + 1 is for string types, and a waste for the rest.
1217 req->outBind[i].buffer_length = fld->length;
1218 req->outBind[i].error = xzalloc(sizeof(my_bool));
1219 req->outBind[i].is_null = xzalloc(sizeof(my_bool));
1220 switch(fld->type)
1221 {
1222 case MYSQL_TYPE_TINY:
1223 {
1224//D("TINY %d %s %d", i, fld->name, req->outBind[i].buffer_length);
1225 break;
1226 }
1227
1228 case MYSQL_TYPE_SHORT:
1229 {
1230//D("SHORT %s %d", fld->name, req->outBind[i].buffer_length);
1231 req->outBind[i].is_unsigned = FALSE;
1232 break;
1233 }
1234
1235 case MYSQL_TYPE_INT24:
1236 {
1237//D("INT24 %s %d", fld->name, req->outBind[i].buffer_length);
1238 req->outBind[i].is_unsigned = FALSE;
1239 break;
1240 }
1241
1242 case MYSQL_TYPE_LONG:
1243 {
1244//D("LONG %d %s %d", i, fld->name, req->outBind[i].buffer_length);
1245 req->outBind[i].is_unsigned = FALSE;
1246 break;
1247 }
1248
1249 case MYSQL_TYPE_LONGLONG:
1250 {
1251//D("LONG LONG %s %d", fld->name, req->outBind[i].buffer_length);
1252 req->outBind[i].is_unsigned = FALSE;
1253 break;
1254 }
1255
1256 case MYSQL_TYPE_FLOAT:
1257 {
1258//D("FLOAT %s %d", fld->name, req->outBind[i].buffer_length);
1259 break;
1260 }
1261
1262 case MYSQL_TYPE_DOUBLE:
1263 {
1264//D("DOUBLE %s %d", fld->name, req->outBind[i].buffer_length);
1265 break;
1266 }
1267
1268 case MYSQL_TYPE_NEWDECIMAL:
1269 {
1270//D("NEWDECIMAL %s %d", fld->name, req->outBind[i].buffer_length);
1271 break;
1272 }
1273
1274 case MYSQL_TYPE_TIME:
1275 case MYSQL_TYPE_DATE:
1276 case MYSQL_TYPE_DATETIME:
1277 case MYSQL_TYPE_TIMESTAMP:
1278 {
1279//D("DATETIME %s %d", fld->name, req->outBind[i].buffer_length);
1280 break;
1281 }
1282
1283 case MYSQL_TYPE_STRING:
1284 case MYSQL_TYPE_VAR_STRING:
1285 {
1286//D("STRING %s %d", fld->name, req->outBind[i].buffer_length);
1287 req->outBind[i].length = xzalloc(sizeof(unsigned long));
1288 break;
1289 }
1290
1291 case MYSQL_TYPE_TINY_BLOB:
1292 case MYSQL_TYPE_BLOB:
1293 case MYSQL_TYPE_MEDIUM_BLOB:
1294 case MYSQL_TYPE_LONG_BLOB:
1295 {
1296//D("BLOB %s %d", fld->name, req->outBind[i].buffer_length);
1297 break;
1298 }
1299
1300 case MYSQL_TYPE_BIT:
1301 {
1302//D("BIT %s %d", fld->name, req->outBind[i].buffer_length);
1303 break;
1304 }
1305
1306 case MYSQL_TYPE_NULL:
1307 {
1308//D("NULL %s %d", fld->name, req->outBind[i].buffer_length);
1309 break;
1310 }
1311 }
1312 }
1313 }
1314 if (mysql_stmt_bind_result(req->prep, req->outBind))
1315 {
1316 E("Bind failed.");
1317 goto freeIt;
1318 }
1319 }
1320 }
1321
1322
1323//D("input bind for %s", req->sql);
1324 for (i = 0; i < req->inCount; i++)
1325 {
1326 dbField *fld = req->flds->get(req->flds, req->inParams[i], NULL, false);
1327
1328 if (NULL == fld)
1329 {
1330 E("Unknown input field %s.%s for - %s", req->table, req->inParams[i], req->sql);
1331 goto freeIt;
1332 }
1333 else
1334 {
1335 switch(fld->type)
1336 {
1337 case MYSQL_TYPE_TINY:
1338 {
1339 int c = va_arg(ap, int);
1340 signed char d = (signed char) c;
1341
1342 memcpy(&d, req->inBind[i].buffer, (size_t) fld->length);
1343 break;
1344 }
1345
1346 case MYSQL_TYPE_SHORT:
1347 {
1348 int c = va_arg(ap, int);
1349 short int d = (short int) c;
1350
1351 memcpy(&d, req->inBind[i].buffer, (size_t) fld->length);
1352 break;
1353 }
1354
1355 case MYSQL_TYPE_INT24:
1356 {
1357 int d = va_arg(ap, int);
1358
1359 memcpy(&d, req->inBind[i].buffer, (size_t) fld->length);
1360 break;
1361 }
1362
1363 case MYSQL_TYPE_LONG:
1364 {
1365 long d = va_arg(ap, long);
1366
1367 memcpy(&d, req->inBind[i].buffer, (size_t) fld->length);
1368 break;
1369 }
1370
1371 case MYSQL_TYPE_LONGLONG:
1372 {
1373 long long int d = va_arg(ap, long long int);
1374
1375 memcpy(&d, req->inBind[i].buffer, (size_t) fld->length);
1376 break;
1377 }
1378
1379 case MYSQL_TYPE_FLOAT:
1380 {
1381 double c = va_arg(ap, double);
1382 float d = (float) c;
1383
1384 memcpy(&d, req->inBind[i].buffer, (size_t) fld->length);
1385 break;
1386 }
1387
1388 case MYSQL_TYPE_DOUBLE:
1389 {
1390 double d = va_arg(ap, double);
1391
1392 memcpy(&d, req->inBind[i].buffer, (size_t) fld->length);
1393 break;
1394 }
1395
1396 case MYSQL_TYPE_NEWDECIMAL:
1397 {
1398 break;
1399 }
1400
1401 case MYSQL_TYPE_TIME:
1402 case MYSQL_TYPE_DATE:
1403 case MYSQL_TYPE_DATETIME:
1404 case MYSQL_TYPE_TIMESTAMP:
1405 {
1406 MYSQL_TIME d = va_arg(ap, MYSQL_TIME);
1407
1408 memcpy(&d, req->inBind[i].buffer, (size_t) fld->length);
1409 break;
1410 }
1411
1412 case MYSQL_TYPE_STRING:
1413 case MYSQL_TYPE_VAR_STRING:
1414 {
1415 char *d = va_arg(ap, char *);
1416 unsigned long l = strlen(d);
1417
1418 if (l > fld->length)
1419 l = fld->length;
1420 *(req->inBind[i].length) = l;
1421 strncpy(req->inBind[i].buffer, d, (size_t) l);
1422 ((char *) req->inBind[i].buffer)[l] = '\0';
1423 break;
1424 }
1425
1426 case MYSQL_TYPE_TINY_BLOB:
1427 case MYSQL_TYPE_BLOB:
1428 case MYSQL_TYPE_MEDIUM_BLOB:
1429 case MYSQL_TYPE_LONG_BLOB:
1430 {
1431 break;
1432 }
1433
1434 case MYSQL_TYPE_BIT:
1435 {
1436 break;
1437 }
1438
1439 case MYSQL_TYPE_NULL:
1440 {
1441 break;
1442 }
1443 }
1444 }
1445 }
1446 if (mysql_stmt_bind_param(req->prep, req->inBind))
1447 {
1448 E("Bind failed.");
1449 goto freeIt;
1450 }
1451
1452
1453D("Execute %s", req->sql);
1454
1455 // do the prepared statement req->prep.
1456 if (mysql_stmt_execute(req->prep))
1457 {
1458 E("Statement execute failed: %s\n", mysql_stmt_error(req->prep));
1459 goto freeIt;
1460 }
1461
1462 int fs = mysql_stmt_field_count(req->prep);
1463 // stuff results back into req.
1464 if (NULL != req->outBind)
1465 {
1466 req->rows = xmalloc(sizeof(rowData));
1467 req->rows->fieldNames = xzalloc(fs * sizeof(char *));
1468 if (mysql_stmt_store_result(req->prep))
1469 {
1470 E(" mysql_stmt_store_result() failed %s", mysql_stmt_error(req->prep));
1471 goto freeIt;
1472 }
1473 req->rowCount = mysql_stmt_num_rows(req->prep);
1474 if (0 == req->rowCount)
1475 D("No rows returned from : %s\n", req->sql);
1476 else
1477 D("%d rows of %d fields returned from : %s\n", req->rowCount, fs, req->sql);
1478
1479 req->rows->rows = qlist(0);
1480 while (MYSQL_NO_DATA != mysql_stmt_fetch(req->prep))
1481 {
1482 qhashtbl_t *flds = qhashtbl(0, 0);
1483
1484 for (i = 0; i < req->outCount; i++)
1485 {
1486 dbField *fld = req->flds->get(req->flds, req->outParams[i], NULL, false);
1487
1488 req->rows->fieldNames[i] = fld->name;
1489 if (!*(req->outBind[i].is_null))
1490 {
1491//D("2.8 %s", req->rows->fieldNames[i]);
1492 flds->put(flds, req->rows->fieldNames[i], req->outBind[i].buffer, req->outBind[i].buffer_length);
1493
1494 switch(fld->type)
1495 {
1496 case MYSQL_TYPE_TINY:
1497 {
1498 break;
1499 }
1500
1501 case MYSQL_TYPE_SHORT:
1502 {
1503 char *t = xmprintf("%d", (int) *((int *) req->outBind[i].buffer));
1504 flds->putstr(flds, req->rows->fieldNames[i], t);
1505 break;
1506 }
1507
1508 case MYSQL_TYPE_INT24:
1509 {
1510 char *t = xmprintf("%d", (int) *((int *) req->outBind[i].buffer));
1511 flds->putstr(flds, req->rows->fieldNames[i], t);
1512 break;
1513 }
1514
1515 case MYSQL_TYPE_LONG:
1516 {
1517 if (NULL == req->outBind[i].buffer)
1518 {
1519 E("Field %d %s is NULL", i, fld->name);
1520 goto freeIt;
1521 }
1522 char *t = xmprintf("%d", (int) *((int *) (req->outBind[i].buffer)));
1523//D("Setting %i %s %s", i, fld->name, t);
1524 flds->putstr(flds, req->rows->fieldNames[i], t);
1525 break;
1526 }
1527
1528 case MYSQL_TYPE_LONGLONG:
1529 {
1530 char *t = xmprintf("%d", (int) *((int *) req->outBind[i].buffer));
1531 flds->putstr(flds, req->rows->fieldNames[i], t);
1532 break;
1533 }
1534
1535 case MYSQL_TYPE_FLOAT:
1536 {
1537 break;
1538 }
1539
1540 case MYSQL_TYPE_DOUBLE:
1541 {
1542 break;
1543 }
1544
1545 case MYSQL_TYPE_NEWDECIMAL:
1546 {
1547 break;
1548 }
1549
1550 case MYSQL_TYPE_TIME:
1551 case MYSQL_TYPE_DATE:
1552 case MYSQL_TYPE_DATETIME:
1553 case MYSQL_TYPE_TIMESTAMP:
1554 {
1555 break;
1556 }
1557
1558 case MYSQL_TYPE_STRING:
1559 case MYSQL_TYPE_VAR_STRING:
1560 {
1561 break;
1562 }
1563
1564 case MYSQL_TYPE_TINY_BLOB:
1565 case MYSQL_TYPE_BLOB:
1566 case MYSQL_TYPE_MEDIUM_BLOB:
1567 case MYSQL_TYPE_LONG_BLOB:
1568 {
1569 break;
1570 }
1571
1572 case MYSQL_TYPE_BIT:
1573 {
1574 break;
1575 }
1576
1577 case MYSQL_TYPE_NULL:
1578 {
1579 break;
1580 }
1581 }
1582 }
1583 else
1584 {
1585 D("Not setting data %s, coz it's NULL", fld->name);
1586 }
1587 }
1588 req->rows->rows->addlast(req->rows->rows, flds, sizeof(*flds));
1589 }
1590 }
1591
1592freeIt:
1593 if (prepare_meta_result)
1594 mysql_free_result(prepare_meta_result);
1595 if (mysql_stmt_free_result(req->prep))
1596 E("Statement result freeing failed: %s\n", mysql_stmt_error(req->prep));
1597
1598end:
1599 va_end(ap);
1600
1601 if (-1 == clock_gettime(CLOCK_REALTIME, &now))
1602 perror_msg("Unable to get the time.");
1603 double n = (now.tv_sec * 1000000000.0) + now.tv_nsec;
1604 double t = (then.tv_sec * 1000000000.0) + then.tv_nsec;
1605 T("dbDoSomething(%s) took %lf seconds", req->sql, (n - t) / 1000000000.0);
1606 return;
1607}
1608
1609// Copy the SQL results into the request structure.
1610void dbPull(reqData *Rd, char *table, rowData *rows)
1611{
1612 char *where;
1613 qhashtbl_t *me = rows->rows->popfirst(rows->rows, NULL);
1614 qhashtbl_obj_t obj;
1615
1616 memset((void*)&obj, 0, sizeof(obj));
1617 me->lock(me);
1618 while(me->getnext(me, &obj, true) == true)
1619 {
1620 where = xmprintf("%s.%s", table, obj.name);
1621 Rd->database->putstr(Rd->database, where, (char *) obj.data);
1622 me->remove(me, obj.name);
1623 free(where);
1624 }
1625 me->unlock(me);
1626 free(me);
1627}
1628
1629/*
1630void dbFreeRequest(dbRequest *req)
1631{
1632 int i;
1633
1634 if (NULL != req->outBind)
1635 {
1636 for (i = 0; i < req->outCount; i++)
1637 {
1638 if (NULL != req->outBind[i].buffer) free(req->outBind[i].buffer);
1639 if (NULL != req->outBind[i].length) free(req->outBind[i].length);
1640 if (NULL != req->outBind[i].error) free(req->outBind[i].error);
1641 if (NULL != req->outBind[i].is_null) free(req->outBind[i].is_null);
1642 }
1643 free(req->outBind);
1644 }
1645 if (NULL != req->inBind)
1646 {
1647 for (i = 0; i < req->inCount; i++)
1648 {
1649 if (NULL != req->inBind[i].buffer) free(req->inBind[i].buffer);
1650 if (NULL != req->inBind[i].length) free(req->inBind[i].length);
1651 if (NULL != req->inBind[i].error) free(req->inBind[i].error);
1652 if (NULL != req->inBind[i].is_null) free(req->inBind[i].is_null);
1653 }
1654 free(req->inBind);
1655 }
1656
1657 if (req->freeOutParams) free(req->outParams);
1658 if (NULL != req->sql) free(req->sql);
1659 if (NULL != req->prep) mysql_stmt_close(req->prep);
1660}
1661*/
1662
1663my_ulonglong dbCount(MYSQL *db, char *table, char *where)
1664{
1665 my_ulonglong ret = 0;
1666 char *sql;
1667 struct timespec now, then;
1668
1669 if (-1 == clock_gettime(CLOCK_REALTIME, &then))
1670 perror_msg("Unable to get the time.");
1671
1672 if (where)
1673 sql = xmprintf("SELECT Count(*) FROM %s WHERE %s", table, where);
1674 else
1675 sql = xmprintf("SELECT Count(*) FROM %s", table);
1676
1677 if (mysql_query(db, sql))
1678 E("Query failed: %s", mysql_error(db));
1679 else
1680 {
1681 MYSQL_RES *result = mysql_store_result(db);
1682
1683 if (!result)
1684 E("Couldn't get results set from %s\n: %s", sql, mysql_error(db));
1685 else
1686 {
1687 MYSQL_ROW row = mysql_fetch_row(result);
1688 if (!row)
1689 E("Couldn't get row from %s\n: %s", sql, mysql_error(db));
1690 else
1691 ret = atoll(row[0]);
1692 mysql_free_result(result);
1693 }
1694 }
1695
1696 if (-1 == clock_gettime(CLOCK_REALTIME, &now))
1697 perror_msg("Unable to get the time.");
1698 double n = (now.tv_sec * 1000000000.0) + now.tv_nsec;
1699 double t = (then.tv_sec * 1000000000.0) + then.tv_nsec;
1700 T("dbCount(%s) took %lf seconds", sql, (n - t) / 1000000000.0);
1701 free(sql);
1702 return ret;
1703}
1704
1705my_ulonglong dbCountJoin(MYSQL *db, char *table, char *select, char *join, char *where)
1706{
1707 my_ulonglong ret = 0;
1708 char *sql;
1709 struct timespec now, then;
1710
1711 if (-1 == clock_gettime(CLOCK_REALTIME, &then))
1712 perror_msg("Unable to get the time.");
1713
1714 if (NULL == select)
1715 select = "*";
1716 if (NULL == join)
1717 join = "";
1718
1719 if (where)
1720 sql = xmprintf("SELECT %s FROM %s %s WHERE %s", select, table, join, where);
1721 else
1722 sql = xmprintf("SELECT %s FROM %s", select, table, join);
1723
1724 if (mysql_query(db, sql))
1725 E("Query failed: %s", mysql_error(db));
1726 else
1727 {
1728 MYSQL_RES *result = mysql_store_result(db);
1729
1730 if (!result)
1731 E("Couldn't get results set from %s\n: %s", sql, mysql_error(db));
1732 else
1733 ret = mysql_num_rows(result);
1734 mysql_free_result(result);
1735 }
1736
1737 if (-1 == clock_gettime(CLOCK_REALTIME, &now))
1738 perror_msg("Unable to get the time.");
1739 double n = (now.tv_sec * 1000000000.0) + now.tv_nsec;
1740 double t = (then.tv_sec * 1000000000.0) + then.tv_nsec;
1741 T("dbCointJoin(%s) took %lf seconds", sql, (n - t) / 1000000000.0);
1742 free(sql);
1743 return ret;
1744}
1745
1746MYSQL_RES *dbSelect(MYSQL *db, char *table, char *select, char *join, char *where, char *order)
1747{
1748 MYSQL_RES *ret = NULL;
1749 char *sql;
1750 struct timespec now, then;
1751
1752 if (-1 == clock_gettime(CLOCK_REALTIME, &then))
1753 perror_msg("Unable to get the time.");
1754
1755 if (NULL == select)
1756 select = "*";
1757 if (NULL == join)
1758 join = "";
1759
1760 if (where)
1761 sql = xmprintf("SELECT %s FROM %s %s WHERE %s", select, table, join, where);
1762 else
1763 sql = xmprintf("SELECT %s FROM %s", select, table, join);
1764
1765 if (order)
1766 {
1767 char *t = xmprintf("%s ORDER BY %s", sql, order);
1768
1769 free(sql);
1770 sql = t;
1771 }
1772
1773 if (mysql_query(db, sql))
1774 E("Query failed: %s\n%s", mysql_error(db), sql);
1775 else
1776 {
1777 ret = mysql_store_result(db);
1778 if (!ret)
1779 E("Couldn't get results set from %s\n %s", mysql_error(db), sql);
1780 }
1781
1782 if (-1 == clock_gettime(CLOCK_REALTIME, &now))
1783 perror_msg("Unable to get the time.");
1784 double n = (now.tv_sec * 1000000000.0) + now.tv_nsec;
1785 double t = (then.tv_sec * 1000000000.0) + then.tv_nsec;
1786 T("dbSelect(%s) took %lf seconds", sql, (n - t) / 1000000000.0);
1787 free(sql);
1788 return ret;
1789}
1790
1791
1792void replaceStr(qhashtbl_t *ssi, char *key, char *value)
1793{
1794 ssi->putstr(ssi, key, value);
1795}
1796
1797void replaceLong(qhashtbl_t *ssi, char *key, my_ulonglong value)
1798{
1799 char *tmp = xmprintf("%lu", value);
1800
1801 replaceStr(ssi, key, tmp);
1802 free(tmp);
1803}
1804
1805
1806float timeDiff(struct timeval *now, struct timeval *then)
1807{
1808 if (0 == gettimeofday(now, NULL))
1809 {
1810 struct timeval thisTime = { 0, 0 };
1811 double result = 0.0;
1812
1813 thisTime.tv_sec = now->tv_sec;
1814 thisTime.tv_usec = now->tv_usec;
1815 if (thisTime.tv_usec < then->tv_usec)
1816 {
1817 thisTime.tv_sec--;
1818 thisTime.tv_usec += 1000000;
1819 }
1820 thisTime.tv_usec -= then->tv_usec;
1821 thisTime.tv_sec -= then->tv_sec;
1822 result = ((double) thisTime.tv_usec) / ((double) 1000000.0);
1823 result += thisTime.tv_sec;
1824 return result;
1825 }
1826
1827 return 0.0;
1828}
1829
1830
1831gridStats *getStats(MYSQL *db, gridStats *stats)
1832{
1833 if (NULL == stats)
1834 {
1835 stats = xmalloc(sizeof(gridStats));
1836 stats->next = 300;
1837 gettimeofday(&(stats->last), NULL);
1838 stats->stats = qhashtbl(0, 0);
1839 stats->stats->putstr(stats->stats, "version", "SledjChisl FCGI Dev 0.1");
1840 stats->stats->putstr(stats->stats, "grid", "my grid");
1841 stats->stats->putstr(stats->stats, "uri", "http://localhost:8002/");
1842// TODO - figure out how to do this. Do this once ROBUST is fixed to actually store it's PID -
1843// if (checkSimIsRunning("ROBUST"))
1844 stats->stats->putstr(stats->stats, "gridOnline", "??");
1845 }
1846 else
1847 {
1848 static struct timeval thisTime;
1849 if (stats->next > timeDiff(&thisTime, &(stats->last)))
1850 return stats;
1851 }
1852
1853 if (db)
1854 {
1855 I("Getting fresh grid stats.");
1856 char *tmp;
1857 my_ulonglong locIn = dbCount(db, "Presence", "RegionID != '00000000-0000-0000-0000-000000000000'"); // Locals online but not HGing, and HGers in world.
1858 my_ulonglong HGin = dbCount(db, "Presence", "UserID NOT IN (SELECT PrincipalID FROM UserAccounts)"); // HGers in world.
1859
1860 // Collect stats about members.
1861 replaceLong(stats->stats, "hgers", HGin);
1862 replaceLong(stats->stats, "inworld", locIn - HGin);
1863 tmp = xmprintf("GridExternalName != '%s'", stats->stats->getstr(stats->stats, "uri", false));
1864 replaceLong(stats->stats, "outworld", dbCount(db, "hg_traveling_data", tmp));
1865 free(tmp);
1866 replaceLong(stats->stats, "members", dbCount(db, "UserAccounts", NULL));
1867
1868 // Count local and HG visitors for the last 30 and 60 days.
1869 locIn = dbCountJoin(db, "GridUser", "GridUser.UserID", "INNER JOIN UserAccounts ON GridUser.UserID = UserAccounts.PrincipalID",
1870 "Login > UNIX_TIMESTAMP(FROM_UNIXTIME(UNIX_TIMESTAMP(now()) - 2419200))");
1871 HGin = dbCount(db, "GridUser", "Login > UNIX_TIMESTAMP(FROM_UNIXTIME(UNIX_TIMESTAMP(now()) - 2419200))");
1872 replaceLong(stats->stats, "locDay30", locIn);
1873 replaceLong(stats->stats, "day30", HGin);
1874 replaceLong(stats->stats, "HGday30", HGin - locIn);
1875
1876 locIn = dbCountJoin(db, "GridUser", "GridUser.UserID", "INNER JOIN UserAccounts ON GridUser.UserID = UserAccounts.PrincipalID",
1877 "Login > UNIX_TIMESTAMP(FROM_UNIXTIME(UNIX_TIMESTAMP(now()) - 4838400))");
1878 HGin = dbCount(db, "GridUser", "Login > UNIX_TIMESTAMP(FROM_UNIXTIME(UNIX_TIMESTAMP(now()) - 4838400))");
1879 replaceLong(stats->stats, "locDay60", locIn);
1880 replaceLong(stats->stats, "day60", HGin);
1881 replaceLong(stats->stats, "HGday60", HGin - locIn);
1882
1883 // Collect stats about sims.
1884 replaceLong(stats->stats, "sims", dbCount(db, "regions", NULL));
1885 replaceLong(stats->stats, "onlineSims", dbCount(db, "regions", "sizeX != 0"));
1886 replaceLong(stats->stats, "varRegions", dbCount(db, "regions", "sizeX > 256 or sizeY > 256"));
1887 replaceLong(stats->stats, "singleSims", dbCount(db, "regions", "sizeX = 256 and sizeY = 256"));
1888 replaceLong(stats->stats, "offlineSims", dbCount(db, "regions", "sizeX = 0"));
1889
1890 // Calculate total size of all regions.
1891 my_ulonglong simSize = 0;
1892 static dbRequest *rgnSizes = NULL;
1893 if (NULL == rgnSizes)
1894 {
1895 static char *szi[] = {NULL};
1896 static char *szo[] = {"sizeX", "sizeY", NULL};
1897 rgnSizes = xzalloc(sizeof(dbRequest));
1898 rgnSizes->db = db;
1899 rgnSizes->table = "regions";
1900 rgnSizes->inParams = szi;
1901 rgnSizes->outParams = szo;
1902 rgnSizes->where = "sizeX != 0";
1903 }
1904 dbDoSomething(rgnSizes, FALSE);
1905 rowData *rows = rgnSizes->rows;
1906 qlist_obj_t obj;
1907 memset((void*)&obj, 0, sizeof(obj)); // must be cleared before call
1908 rows->rows->lock(rows->rows);
1909 while (rows->rows->getnext(rows->rows, &obj, false) == true)
1910 {
1911 qhashtbl_t *row = (qhashtbl_t *) obj.data;
1912 my_ulonglong x = 0, y = 0;
1913
1914 tmp = row->getstr(row, "sizeX", false);
1915 if (NULL == tmp)
1916 E("No regions.sizeX!");
1917 else
1918 x = atoll(tmp);
1919 tmp = row->getstr(row, "sizeY", false);
1920 if (NULL == tmp)
1921 E("No regions.sizeY!");
1922 else
1923 y = atoll(tmp);
1924 simSize += x * y;
1925 free(row);
1926 }
1927 rows->rows->unlock(rows->rows);
1928 free(rows->rows);
1929 free(rows->fieldNames);
1930 free(rows);
1931
1932 tmp = xmprintf("%lu", simSize);
1933 stats->stats->putstr(stats->stats, "simsSize", tmp);
1934 free(tmp);
1935 gettimeofday(&(stats->last), NULL);
1936 }
1937
1938 return stats;
1939}
1940
1941
1942qhashtbl_t *toknize(char *text, char *delims)
1943{
1944 qhashtbl_t *ret = qhashtbl(0, 0);
1945
1946 if (NULL == text)
1947 return ret;
1948
1949 char *txt = xstrdup(text), *token, dlm = ' ', *key, *val = NULL;
1950 int offset = 0;
1951
1952 while((token = qstrtok(txt, delims, &dlm, &offset)) != NULL)
1953 {
1954 if (delims[0] == dlm)
1955 {
1956 key = token;
1957 val = &txt[offset];
1958 }
1959 else if (delims[1] == dlm)
1960 {
1961 ret->putstr(ret, qstrtrim_head(key), token);
1962D(" %s = %s", qstrtrim_head(key), val);
1963 val = NULL;
1964 }
1965 }
1966 if (NULL != val)
1967{
1968 ret->putstr(ret, qstrtrim_head(key), val);
1969D(" %s = %s", qstrtrim_head(key), val);
1970}
1971 free(txt);
1972 return ret;
1973}
1974
1975void santize(qhashtbl_t *tbl, bool decode)
1976{
1977 qhashtbl_obj_t obj;
1978
1979 memset((void*)&obj, 0, sizeof(obj));
1980 tbl->lock(tbl);
1981 while(tbl->getnext(tbl, &obj, true) == true)
1982 {
1983 char *n = obj.name, *o = (char *) obj.data;
1984
1985 if (decode)
1986 qurl_decode(o);
1987
1988// if ((strcmp(n, "password") != 0) && (strcmp(n, "psswd") != 0))
1989 {
1990 // Poor mans Bobby Tables protection.
1991 o = qstrreplace("tr", o, "'", "_");
1992 o = qstrreplace("tr", o, "\"", "_");
1993 o = qstrreplace("tr", o, ";", "_");
1994 o = qstrreplace("tr", o, "(", "_");
1995 o = qstrreplace("tr", o, ")", "_");
1996 }
1997
1998 tbl->putstr(tbl, n, o);
1999 free(o);
2000 }
2001 tbl->unlock(tbl);
2002}
2003
2004void outize(qgrow_t *reply, qhashtbl_t *tbl, char *label)
2005{
2006 reply->addstrf(reply, "%s:<br>\n<pre>\n", label);
2007 qhashtbl_obj_t obj;
2008 memset((void*)&obj, 0, sizeof(obj));
2009 tbl->lock(tbl);
2010 while(tbl->getnext(tbl, &obj, false) == true)
2011 reply->addstrf(reply, " &nbsp; %s = %s\n", obj.name, (char *) obj.data);
2012 tbl->unlock(tbl);
2013 reply->addstr(reply, "</pre>\n");
2014}
2015
2016
2017// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie
2018enum cookieSame
2019{
2020 CS_NOT,
2021 CS_STRICT,
2022 CS_LAX, // Apparently the default set by browsers these days.
2023 CS_NONE
2024};
2025typedef struct _cookie cookie;
2026struct _cookie
2027{
2028 char *cookie, *value, *domain, *path;
2029 // char *expires; // Use maxAge instead, it's far simpler to figure out.
2030 int maxAge;
2031 boolean secure, httpOnly;
2032 enum cookieSame site;
2033};
2034
2035cookie *setCookie(reqData *Rd, char *cki, char *value)
2036{
2037 cookie *ret = xzalloc(sizeof(cookie));
2038 int l, i;
2039
2040 ret->cookie = xstrdup(cki);
2041 // Validate this, as there is a limited set of characters allowed.
2042 qstrreplace("tr", ret->cookie, "()<>@,;:\\\"/[]?={} \t", "_");
2043 l = strlen(ret->cookie);
2044 for (i = 0; i < l; i++)
2045 {
2046 if (iscntrl(ret->cookie[i]) != 0)
2047 ret->cookie[i] = '_';
2048 }
2049 ret->value = qurl_encode(value, strlen(value));
2050 ret->httpOnly = TRUE;
2051 ret->site = CS_STRICT;
2052 ret->secure = TRUE;
2053 ret->path = xstrdup(getStrH(Rd->headers, "SCRIPT_NAME"));
2054 Rd->Rcookies->put(Rd->Rcookies, cki, ret, sizeof(*ret));
2055
2056 return ret;
2057}
2058
2059char *getCookie(qhashtbl_t *cookies, char *cki)
2060{
2061 char *ret = NULL;
2062 cookie *ck = (cookie *) cookies->get(cookies, cki, NULL, false);
2063
2064 if (NULL != ck)
2065 ret = ck->value;
2066
2067 return ret;
2068}
2069
2070void outizeCookie(qgrow_t *reply, qhashtbl_t *tbl, char *label)
2071{
2072 reply->addstrf(reply, "%s:<br>\n<pre>\n", label);
2073 qhashtbl_obj_t obj;
2074 memset((void*)&obj, 0, sizeof(obj));
2075 tbl->lock(tbl);
2076 while(tbl->getnext(tbl, &obj, false) == true)
2077 reply->addstrf(reply, " &nbsp; %s = %s\n", obj.name, ((cookie *) obj.data)->value);
2078 tbl->unlock(tbl);
2079 reply->addstr(reply, "</pre>\n");
2080}
2081
2082
2083
2084enum fragmentType
2085{
2086 FT_TEXT,
2087 FT_PARAM,
2088 FT_LUA
2089};
2090
2091typedef struct _fragment fragment;
2092struct _fragment
2093{
2094 enum fragmentType type;
2095 int length;
2096 char *text;
2097};
2098
2099static void HTMLheader(qgrow_t *reply, char *title)
2100{
2101 reply->addstrf(reply,
2102 "<html>\n"
2103 " <head>\n"
2104 " <title>%s</title>\n"
2105 " <meta charset=\"UTF-8\">\n"
2106 " <link rel=\"shortcut icon\" href=\"/SledjHamrIconSmall.png\">\n"
2107 " <link type='text/css' rel='stylesheet' href='/debugStyle.css' media='all' />\n"
2108 " <style> \n"
2109 " html, body {background-color: black; color: white; font-family: 'sans-serif'; margin: 0; padding: 0;}\n"
2110 " a:link {color: aqua;}\n"
2111 " a:visited {color: fuchsia;}\n"
2112 " a:hover {color: blue;}\n"
2113 " a:active {color: red;}\n"
2114 " button:hover {color: blue;}\n"
2115 " button:active {color: red;}\n"
2116 " </style>\n"
2117 " </head>\n"
2118 " <body bgcolor='black' text='white' link='aqua' vlink='fuchsia' alink='red'>\n"
2119 " <font face='sans-serif'>"
2120 , title);
2121}
2122
2123static void HTMLdebug(qgrow_t *reply)
2124{
2125 reply->addstrf(reply,
2126 " <div class='top-left'>\n"
2127 " <p class='hoverItem'>\n"
2128 " <div class='hoverWrapper0'>\n"
2129 " <p>DEBUG</p>\n"
2130 " <div id='hoverShow0'>\n"
2131 " <h1>DEBUG log</h1>\n"
2132 " <!--#echo var=\"DEBUG\" -->"
2133 " </div>\n"
2134 " </div>\n"
2135 " </p>\n"
2136 " </div>\n"
2137 );
2138}
2139
2140static void HTMLtable(qgrow_t *reply, MYSQL *db, MYSQL_RES *result, char *caption, char *URL, char *id)
2141{
2142 char *tbl = "";
2143 char *address, *addrend, *t, *t0;
2144 int count = 0, c = -1, i;
2145 MYSQL_ROW row;
2146 MYSQL_FIELD *fields = mysql_fetch_fields(result);
2147
2148 reply->addstrf(reply, "<table border=\"1\"><caption>%s</caption>\n", caption);
2149
2150 if (!fields)
2151 E("Failed fetching fields: %s", mysql_error(db));
2152 while ((row = mysql_fetch_row(result)))
2153 {
2154 reply->addstr(reply, "<tr>");
2155 address = xmprintf("");
2156 addrend = "";
2157
2158 if (-1 == c)
2159 c = mysql_num_fields(result);
2160
2161 if (0 == count)
2162 {
2163 for (i = 0; i < c; i++)
2164 {
2165 char *s = fields[i].name;
2166
2167 reply->addstrf(reply, "<th>%s</th>", s);
2168 }
2169 reply->addstr(reply, "</tr>\n<tr>");
2170 }
2171
2172 if (NULL != URL)
2173 {
2174 free(address);
2175 address = xmprintf("<a href=\"%s", URL);
2176 addrend = "</a>";
2177 }
2178
2179 for (i = 0; i < c; i++)
2180 {
2181 char *s = fields[i].name;
2182
2183 t0 = row[i];
2184 if (NULL == t0)
2185 E("No field %s!", s);
2186 else
2187 {
2188 if ((NULL != id) && (strcmp(s, id) == 0))
2189 reply->addstrf(reply, "<td>%s&%s=%s\">%s%s</td>", address, id, t0, t0, addrend);
2190 else
2191 reply->addstrf(reply, "<td>%s</td>", t0);
2192 }
2193 }
2194 reply->addstr(reply, "</tr>\n");
2195
2196 free(address);
2197 count++;
2198 }
2199
2200 reply->addstr(reply, "</table>");
2201 mysql_free_result(result);
2202}
2203
2204static void HTMLhidden(qgrow_t *reply, char *name, char *val)
2205{
2206 reply->addstrf(reply, " <input type=\"hidden\" name=\"%s\" value=\"%s\">\n", name, val);
2207}
2208
2209static void HTMLform(qgrow_t *reply, char *action, char *token)
2210{
2211 reply->addstrf(reply, " <form action=\"%s\" method=\"POST\">\n", action);
2212 if ((NULL != token) && ('\0' != token[0]))
2213 HTMLhidden(reply, "munchie", token);
2214}
2215static void HTMLformEnd(qgrow_t *reply)
2216{
2217 reply->addstr(reply, " </form>\n");
2218}
2219
2220static void HTMLcheckBox(qgrow_t *reply, char *name, char *title, boolean checked)
2221{
2222 if (checked)
2223 reply->addstrf(reply, " <p><input type=\"checkbox\" name=\"%s\" checked><label for=\"%s\">%s</label></p>\n", name, name, title);
2224 else
2225 reply->addstrf(reply, " <p><input type=\"checkbox\" name=\"%s\"><label for=\"%s\">%s</label></p>\n", name, name, title);
2226}
2227
2228static void HTMLtext(qgrow_t *reply, char *type, char *title, char *name, char *val, int size, int max, boolean required)
2229{
2230 reply->addstrf(reply, " <p>%s : <input type=\"%s\" name=\"%s\"", title, type, name);
2231 if ("" != val)
2232 reply->addstrf(reply, "value=\"%s\"", val);
2233 if (0 < size)
2234 reply->addstrf(reply, " size=\"%d\"", size);
2235 if (0 < max)
2236 reply->addstrf(reply, " maxlength=\"%d\"", max);
2237 if (required)
2238 reply->addstr(reply, " required");
2239 reply->addstr(reply, "></p>\n");
2240}
2241
2242static void HTMLselect(qgrow_t *reply, char *title, char *name)
2243{
2244 if (NULL == title)
2245 reply->addstrf(reply, " <select name=\"%s\">", name);
2246 else
2247 reply->addstrf(reply, " <p>%s : \n <select name=\"%s\">\n", title, name);
2248}
2249static void HTMLselectEnd(qgrow_t *reply)
2250{
2251 reply->addstr(reply, " </select>\n </p>\n");
2252}
2253static void HTMLselectEndNo(qgrow_t *reply)
2254{
2255 reply->addstr(reply, " </select>");
2256}
2257
2258static void HTMLoption(qgrow_t *reply, char *title, boolean selected)
2259{
2260 char *sel = "";
2261
2262 if (selected)
2263 sel = " selected";
2264 reply->addstrf(reply, " <option value=\"%s\"%s>%s</option>\n", title, sel, title);
2265}
2266
2267static void HTMLbutton(qgrow_t *reply, char *title)
2268{
2269 reply->addstrf(reply, " <button type=\"submit\" name=\"doit\" value=\"%s\">%s</button>\n", title, title);
2270}
2271
2272static void HTMLlist(qgrow_t *reply, char *title, qlist_t *list)
2273{
2274 qlist_obj_t obj;
2275
2276 reply->addstrf(reply, "<ul>%s\n", title);
2277 memset((void*)&obj, 0, sizeof(obj)); // must be cleared before call
2278 list->lock(list);
2279 while (list->getnext(list, &obj, false) == true)
2280 reply->addstrf(reply, "<li>%s</li>\n", (char *) obj.data);
2281 list->unlock(list);
2282 reply->addstr(reply, "</ul>\n");
2283}
2284
2285static int count = 0;
2286void HTMLfill(reqData *Rd, enum fragmentType type, char *text, int length)
2287{
2288 char *tmp;
2289
2290 switch (type)
2291 {
2292 case FT_TEXT:
2293 {
2294 if (length)
2295 Rd->reply->add(Rd->reply, (void *) text, length * sizeof(char));
2296 break;
2297 }
2298
2299 case FT_PARAM:
2300 {
2301 if (strcmp("DEBUG", text) == 0)
2302 {
2303 Rd->reply->addstrf(Rd->reply, "<h1>FastCGI SledjChisl</h1>\n"
2304 "<p>Request number %d, Process ID: %d</p>\n", count++, getpid());
2305 Rd->reply->addstrf(Rd->reply, "<p>libfcgi version: %s</p>\n", FCGI_VERSION);
2306 Rd->reply->addstrf(Rd->reply, "<p>Lua version: %s</p>\n", LUA_RELEASE);
2307 Rd->reply->addstrf(Rd->reply, "<p>LuaJIT version: %s</p>\n", LUAJIT_VERSION);
2308 Rd->reply->addstrf(Rd->reply, "<p>MySQL client version: %s</p>\n", mysql_get_client_info());
2309 outize(Rd->reply, Rd->headers, "Environment");
2310 outize(Rd->reply, Rd->cookies, "Cookies");
2311 outize(Rd->reply, Rd->queries, "Query");
2312 outize(Rd->reply, Rd->body, "POST body");
2313 outize(Rd->reply, Rd->stuff, "Stuff");
2314 showSesh(Rd->reply, &Rd->shs);
2315 if (Rd->lnk) showSesh(Rd->reply, Rd->lnk);
2316 outize(Rd->reply, Rd->database, "Database");
2317 outizeCookie(Rd->reply, Rd->Rcookies, "Reply Cookies");
2318 outize(Rd->reply, Rd->Rheaders, "Reply HEADERS");
2319 }
2320 else if (strcmp("URL", text) == 0)
2321 Rd->reply->addstrf(Rd->reply, "%s://%s%s", Rd->Scheme, Rd->Host, Rd->Script);
2322 else
2323 {
2324 if ((tmp = Rd->stats->stats->getstr(Rd->stats->stats, text, false)) != NULL)
2325 Rd->reply->addstr(Rd->reply, tmp);
2326 else
2327 Rd->reply->addstrf(Rd->reply, "<b>%s</b>", text);
2328 }
2329 break;
2330 }
2331
2332 case FT_LUA:
2333 break;
2334 }
2335}
2336
2337static void HTMLfooter(qgrow_t *reply)
2338{
2339 reply->addstr(reply,
2340 " </font>"
2341 " </body>\n</html>\n");
2342}
2343
2344
2345fragment *newFragment(enum fragmentType type, char *text, int len)
2346{
2347 fragment *frg = xmalloc(sizeof(fragment));
2348 frg->type = type;
2349 frg->length = len;
2350 frg->text = xmalloc(len + 1);
2351 memcpy(frg->text, text, len);
2352 frg->text[len] = '\0';
2353 return frg;
2354}
2355
2356qlist_t *fragize(char *mm, size_t length)
2357{
2358 qlist_t *fragments = qlist(QLIST_THREADSAFE);
2359 fragment *frg0, *frg1;
2360
2361 char *h;
2362 int i, j = 0, k = 0, l, m;
2363
2364 // Scan for server side includes style markings.
2365 for (i = 0; i < length; i++)
2366 {
2367 if (i + 5 < length)
2368 {
2369 if (('<' == mm[i]) && ('!' == mm[i + 1]) && ('-' == mm[i + 2]) && ('-' == mm[i + 3]) && ('#' == mm[i + 4])) // '<!--#'
2370 {
2371 m = i;
2372 i += 5;
2373 if (i < length)
2374 {
2375 if (('e' == mm[i]) && ('c' == mm[i + 1]) && ('h' == mm[i + 2]) && ('o' == mm[i + 3]) && (' ' == mm[i + 4])) // 'echo '
2376 {
2377 i += 5;
2378 if (i + 5 < length)
2379 {
2380 if (('v' == mm[i]) && ('a' == mm[i + 1]) && ('r' == mm[i + 2]) && ('=' == mm[i + 3]) && ('"' == mm[i + 4])) // 'var="'
2381 {
2382 i += 5;
2383 for (j = i; j < length; j++)
2384 {
2385 if ('"' == mm[j]) // '"'
2386 {
2387 frg1 = newFragment(FT_PARAM, &mm[i], j - i);
2388 i = j + 1;
2389 if (i + 4 < length)
2390 {
2391 if ((' ' == mm[i]) && ('-' == mm[i + 1]) && ('-' == mm[i + 2]) && ('>' == mm[i + 3])) // ' -->'
2392 i += 4;
2393 }
2394 frg0 = newFragment(FT_TEXT, &mm[k], m - k);
2395 fragments->addlast(fragments, frg0, sizeof(*frg0));
2396 fragments->addlast(fragments, frg1, sizeof(*frg1));
2397 k = i;
2398 break;
2399 }
2400 }
2401 }
2402 }
2403 }
2404 }
2405 }
2406 }
2407 }
2408 frg0 = newFragment(FT_TEXT, &mm[k], length - k);
2409 fragments->addlast(fragments, frg0, sizeof(*frg0));
2410
2411 return fragments;
2412}
2413
2414void unfragize(qlist_t *fragments, reqData *Rd)
2415{
2416 qlist_obj_t lobj;
2417 memset((void *) &lobj, 0, sizeof(lobj));
2418 fragments->lock(fragments);
2419 while (fragments->getnext(fragments, &lobj, false) == true)
2420 {
2421 fragment *frg = (fragment *) lobj.data;
2422 if (NULL == frg->text)
2423 {
2424 E("NULL fragment!");
2425 continue;
2426 }
2427 HTMLfill(Rd, frg->type, frg->text, frg->length);
2428 }
2429 fragments->unlock(fragments);
2430}
2431
2432HTMLfile *checkHTMLcache(char *file)
2433{
2434 if (NULL == HTMLfileCache)
2435 HTMLfileCache = qhashtbl(0, 0);
2436
2437 HTMLfile *ret = (HTMLfile *) HTMLfileCache->get(HTMLfileCache, file, NULL, false);
2438 int fd = open(file, O_RDONLY);
2439 size_t length = 0;
2440
2441 if (-1 == fd)
2442 {
2443 HTMLfileCache->remove(HTMLfileCache, file);
2444 free(ret);
2445 ret = NULL;
2446 }
2447 else
2448 {
2449 struct stat sb;
2450 if (fstat(fd, &sb) == -1)
2451 {
2452 HTMLfileCache->remove(HTMLfileCache, file);
2453 free(ret);
2454 ret = NULL;
2455 E("Failed to stat %s", file);
2456 }
2457 else
2458 {
2459 if ((NULL != ret) && (ret->last.tv_sec < sb.st_mtim.tv_sec))
2460 {
2461 HTMLfileCache->remove(HTMLfileCache, file);
2462 free(ret);
2463 ret = NULL;
2464 }
2465
2466 if (NULL == ret)
2467 {
2468 char *mm = MAP_FAILED;
2469
2470 ret = xmalloc(sizeof(HTMLfile));
2471 length = sb.st_size;
2472 ret->last.tv_sec = sb.st_mtim.tv_sec;
2473 ret->last.tv_nsec = sb.st_mtim.tv_nsec;
2474
2475 I("Loading web template %s", file);
2476 D("Web template %s is %d bytes long.", file, length);
2477
2478 mm = mmap(NULL, length, PROT_READ, MAP_SHARED | MAP_POPULATE, fd, 0);
2479 if (mm == MAP_FAILED)
2480 {
2481 HTMLfileCache->remove(HTMLfileCache, file);
2482 free(ret);
2483 ret = NULL;
2484 E("Failed to mmap %s", file);
2485 }
2486 else
2487 {
2488 ret->fragments = fragize(mm, length);
2489 if (-1 == munmap(mm, length))
2490 FCGI_fprintf(FCGI_stderr, "Failed to munmap %s\n", file);
2491
2492 HTMLfileCache->put(HTMLfileCache, file, ret, sizeof(*ret));
2493 }
2494 }
2495 close(fd);
2496 }
2497 }
2498
2499 return ret;
2500}
2501
2502
2503/* TODO -
2504
2505 On new user / password reset.
2506. Both should have all the same security concerns as the login page, they are basically logins.
2507. Codes should be "very long", "(for example, 16 case-sensitive alphanumeric characters)"
2508. "confirm" button hit on "accountCreationPage" or "resetPasswordPage"
2509. generate a new token, keep it around for idleTimeOut (or at least 24 hours), call it .linky instead of .lua
2510. hash the linky for the file name, for the same reason we hash the hashish with pepper for the leaf-node.
2511. Include user level field, new users get -200.
2512. Store the linky itself around somewhere we can find it quickly for logged in users.
2513. store it in the regenerated session
2514. Scratch that, we should never store the raw linky, see above about hashing the linky.
2515
2516. The linky is just like the session token, create it in exactly the same way.
2517. Linky is base64() of the binary, so it's short enough to be a file name, and not too long for the URL.
2518. But we only get to send one of them as a linky URL, no backup cookies / body / headers.
2519. Sooo, need to separate the session stuff out of Rd->stuff.
2520. Use two separate qhashtbl's, Rd->session and Rd->linky.
2521
2522 For new user
2523. create their /opt/opensim_SC/var/lib/users/UUID.lua account record, and symlink firstName_lastName.lua to it.
2524. They can log on,
2525 but all they can do is edit their email to send a new validation code, and enter the validation code.
2526 They can reset their password.
2527. Warn them on login and any page refresh that there is an outstanding validation awaiting them.
2528 For reset password
2529. Let them do things as normal, in case this was just someone being mean to them, coz their email addy might be public.
2530. Including the usual logging out and in again with their old password.
2531. Warn them on login and any page refresh that there is an outstanding password reset awaiting them.
2532 email linky, which is some or all of the token result bits strung together, BASE64 encode the result.
2533. regenerate the usual token
2534. user clicks on the linky (or just enters the linky in a field)
2535. validate the linky token.
2536 compare the level field to the linky type in the linky URL, new users -200 would be "../validateUser/.." and not "../resetPassword/.."
2537. delete the linky token
2538. Particularly important for the forgotten password email, since now that token is in the wild, and is used to reset a password.
2539 Which begs the question, other than being able to recieve the email, how do we tell it's them?
2540 Security questions suck, too easily guessed.
2541 Ask their DoB. Still sucky, coz "hey it's my birthday today" is way too leaky.
2542 This is what Multi Factor Autentication is good for, and that's on the TODO list.
2543 Also, admins should have a harder time doing password resets.
2544 Must be approved by another admin?
2545 Must log onto the server via other means to twiddle something there?
2546 For password reset page
2547 Ask their DoB to help confirm it's them.
2548 validate the DoB, delete tokens and back to the login page if they get it wrong
2549 Should warn people on the accountCreationPage that DoB might be used this way.
2550 ask them for the new password, twice
2551 Create a new passwordSalt and passwordHash, store them in the auth table.
2552 For validate new user page
2553. tell them they have validated
2554 create their OpenSim account UserAccounts.UserTitle and auth tables, not GridUser table
2555 create their GridUser record.
2556 update their UserAccounts.Userlevel and UserAccounts.UserTitle
2557. send them to the login page.
2558. regenerate the usual token
2559? let user stay logged on?
2560 Check best practices for this.
2561
2562 Check password strength.
2563 https://stackoverflow.com/questions/549/the-definitive-guide-to-form-based-website-authentication?rq=1
2564 Has some pointers to resources in the top answers "PART V: Checking Password Strength" section.
2565
2566 "PART VI: Much More - Or: Preventing Rapid-Fire Login Attempts" and "PART VII: Distributed Brute Force Attacks" is also good for -
2567 Login attempt throttling.
2568 Deal with dictionary attacks by slowing down access on password failures etc.
2569
2570 Deal with editing yourself.
2571 Deal with editing others, but only as god.
2572
2573
2574 Regularly delete old session files and ancient newbies.
2575
2576
2577 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.
2578 And store hash(salt+password).
2579 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.
2580 So far I have discovered -
2581 On login server side if the password doesn't start with $1$, then password = "$1$" + Util.Md5Hash(passwd);
2582 remove the "$1$ bit
2583 string hashed = Util.Md5Hash(password + ":" + data.Data["passwordSalt"].ToString());
2584 if (data.Data["passwordHash"].ToString() == hashed)
2585 passwordHash is char(32), and as implied above, doesn't include the $1$ bit. passwordSalt is also char(32)
2586 Cool VL and Impy at least sends the $1$ md5 hashed version over the wire.
2587 Modern viewers obfuscate the details buried deep in C++ OOP crap.
2588 Sent via XMLRPC
2589 MD5 is considered broken since 2013, possibly longer.
2590 Otherwise use a slow hashing function. bcrypt? scrypt? Argon2? PBKDF2?
2591 https://security.stackexchange.com/questions/211/how-to-securely-hash-passwords
2592 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.
2593 Which is what the $1$ bit currently used between server and client is sorta for.
2594
2595+ Would be good to have one more level of this Rd->database stuff, so we know the type of the thing.
2596 While qhashtbl has API for putting strings, ints, and memory, it has none for finding out what type a stored thing is.
2597 Once I have a structure, I add things like "level needed to edit it", "OpenSim db structure to Lua file mapping", and other fanciness.
2598 Would also help with the timestamp being stored for the session, it prints out binary gunk in the DEBUG <div>.
2599 Starting to get into object oriented territory here. B-)
2600 I'll have to do it eventually anyway.
2601 object->tostring(object), and replace the big switch() statements in the existing db code with small functions.
2602 That's why the qlibc stuff has that format, coz C doesn't understand the concept of passing "this" as the first argument.
2603 https://stackoverflow.com/questions/351733/how-would-one-write-object-oriented-code-in-c
2604 https://stackoverflow.com/questions/415452/object-orientation-in-c
2605 http://ooc-coding.sourceforge.net/
2606
2607
2608https://owasp.org/www-project-cheat-sheets/cheatsheets/Input_Validation_Cheat_Sheet.html#Email_Address_Validation
2609https://cheatsheetseries.owasp.org/
2610https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html
2611https://owasp.org/www-project-cheat-sheets/cheatsheets/Authentication_Cheat_Sheet.html
2612https://softwareengineering.stackexchange.com/questions/46716/what-technical-details-should-a-programmer-of-a-web-application-consider-before
2613https://wiki.owasp.org/index.php/OWASP_Guide_Project
2614https://stackoverflow.com/questions/549/the-definitive-guide-to-form-based-website-authentication?rq=1
2615*/
2616
2617
2618
2619// Forward declare this here so we can use it in validation functions.
2620void loginPage(reqData *Rd, char *message);
2621
2622/* Four choices for the token - (https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html)
2623 https://en.wikipedia.org/wiki/Cross-site_request_forgery
2624 Has some more info.
2625
2626Large random value generated by a secure method (getrandom(2)).
2627 Keep it secret, put it in hidden fields, or custom HTTP header (requires JavaScript but more secure than hidden fields).
2628 NOT cookies or GET. Don't log it.
2629Cryptographically sign a session ID and timestamp.
2630 Timestamp is for session timeouts.
2631 Keep it secret, put it in hidden fields, or custom HTTP header (requires JavaScript but more secure than hidden fields).
2632 Needs a secret key server side.
2633A strong HMAC (SHA-256 or better) of a session ID and timestamp.
2634 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.
2635 https://en.wikipedia.org/wiki/HMAC says there is a key as well.
2636 https://www.openssl.org/docs/man1.1.0/man3/HMAC.html HAH! They can have keys. OpenSSL's docs suck.
2637 Token = HMAC(sessionID+timestamp)+timestamp (Yes, timestamp is used twice).
2638 Keep it secret, put it in hidden fields, or custom HTTP header (requires JavaScript but more secure than hidden fields).
2639 Needs a secret key server side.
2640Double cookie
2641 Large random value generated by a secure method set as a cookie and hidden field. Check they match.
2642 Optional - encrypt / salted hash it in another cookie / hidden field.
2643+ Also a resin (BASE64 session key in the query string).
2644 Not such a good idea to have something in the query, coz that screws with bookmarks.
2645 https://security.stackexchange.com/questions/59470/double-submit-cookies-vulnerabilities
2646 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".
2647 https://security.stackexchange.com/questions/220797/is-the-double-submit-cookie-pattern-still-effective
2648+ Includes a work around that I might already be doing.
2649
2650SOOOOO - use double cookie + hidden field.
2651 No headers, coz I need JavaScript to do that.
2652 No hidden field when redirecting post POST to GET, coz GOT doesn't get those.
2653 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.
2654 salt = large random value generated by a secure method (getrandom(2)).
2655 seshID = large random value generated by a secure method (getrandom(2)).
2656 timeStamp = mtime of the leaf-node file, set to current time when we are creating the token.
2657 sesh = seshID + timeStamp
2658 munchie = HMAC(sesh) + timeStamp The token hidden field
2659 toke_n_munchie = HMAC(UUID + munchie) The token cookie
2660 hashish = HMACkey(toke_n_munchie, salt) Salted token cookie & linky query
2661? resin = BASE64(hashish) Base64 token cookie
2662 leaf-node = HMACkey(hashish, pepper) Stored token file name
2663
2664 Leaf-node.lua (mtime is timeStamp)
2665 IP, UUID, salt, seshID, user name, passwordSalt, passwordHash (last two for OpenSim password protocol)
2666
2667 The test - (validateSesh() below)
2668 we get hashish and toke_n_munchie
2669 HMACkey(hashish + pepper) -> leaf-node
2670 read leaf-node.lua -> IP, UUID, salt, seshID
2671 get it's mtime -> timeStamp
2672 seshID + timeStamp -> sesh
2673 HMAC(sesh) + timeStamp -> munchie
2674 if we got munchie in the hidden field, compare it
2675 toke_n_munchie == HMAC(UUID + munchie)
2676 For linky it'll be -
2677 HMAC(UUID + munchie) -> toke_n_munchie
2678 hashish == HMACkey(toke_n_munchie + salt)
2679+ If it's too old according to mtime, delete it and logout.
2680
2681I 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.
2682 Same for the pepper.
2683
2684The required JavaScript might be like https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#xmlhttprequest--native-javascript-
2685 NOTE - they somehow fucked up that anchor tag.
2686
2687NOTE - 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.
2688 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.
2689 Also is a problem with SSD and rust storing good data on bad sectors in the spare sector pool, wear levelling, etc.
2690
2691https://stackoverflow.com/questions/16891729/best-practices-salting-peppering-passwords
2692*/
2693
2694static void freeSesh(reqData *Rd, boolean linky, boolean wipe)
2695{
2696 char *file = NULL;
2697 sesh *shs = &Rd->shs;
2698
2699 if (linky)
2700 {
2701 shs = Rd->lnk;
2702 file = xmprintf("%s/caches/sessions/%s.linky", getStrH(Rd->configs, "scRoot"), shs->leaf);
2703 }
2704 else
2705 file = xmprintf("%s/caches/sessions/%s.lua", getStrH(Rd->configs, "scRoot"), shs->leaf);
2706
2707 if (wipe)
2708 I("Wiping session %s.", file);
2709 else
2710 I("Deleting session %s.", file);
2711
2712 if ('\0' != shs->leaf[0])
2713 {
2714 if (unlink(file))
2715 perror_msg("Unable to delete %s", file);
2716 }
2717
2718 Rd->body-> remove(Rd->body, "munchie");
2719
2720 Rd->cookies->remove(Rd->cookies, "toke_n_munchie");
2721 Rd->cookies->remove(Rd->cookies, "hashish");
2722
2723 cookie *ck = setCookie(Rd, "toke_n_munchie", "");
2724 cookie *ckh = setCookie(Rd, "hashish", "");
2725 ck->maxAge = -1; // Should expire immediately.
2726 ckh->maxAge = -1; // Should expire immediately.
2727
2728 qhashtbl_obj_t obj;
2729 memset((void*)&obj, 0, sizeof(obj));
2730 Rd->database->lock(Rd->database);
2731 while(Rd->database->getnext(Rd->database, &obj, false) == true)
2732 Rd->database->remove(Rd->database, obj.name);
2733 Rd->database->unlock(Rd->database);
2734
2735 if (wipe)
2736 {
2737 Rd->stuff->remove(Rd->stuff, "UUID");
2738 Rd->stuff->remove(Rd->stuff, "name");
2739 Rd->stuff->remove(Rd->stuff, "level");
2740 Rd->stuff->remove(Rd->stuff, "passwordSalt");
2741 Rd->stuff->remove(Rd->stuff, "passwordHash");
2742 }
2743
2744 if (shs->isLinky)
2745 {
2746 free(Rd->lnk);
2747 Rd->lnk = NULL;
2748 }
2749 else
2750 shs->leaf[0] = '\0';
2751}
2752
2753static void setToken_n_munchie(reqData *Rd, boolean linky)
2754{
2755 sesh *shs = &Rd->shs;
2756 char *file, *link = "";
2757
2758 if (linky)
2759 {
2760 shs = Rd->lnk;
2761 file = xmprintf("%s/caches/sessions/%s.linky", getStrH(Rd->configs, "scRoot"), shs->leaf);
2762 }
2763 else
2764 {
2765 file = xmprintf("%s/caches/sessions/%s.lua", getStrH(Rd->configs, "scRoot"), shs->leaf);
2766 if (NULL != Rd->lnk)
2767 link = Rd->lnk->hashish;
2768 }
2769
2770 struct stat st;
2771 int s = stat(file, &st);
2772
2773 if (!linky)
2774 {
2775 cookie *ck = setCookie(Rd, "toke_n_munchie", shs->toke_n_munchie);
2776 cookie *ckh = setCookie(Rd, "hashish", shs->hashish);
2777 }
2778 char *tnm = xmprintf( "toke_n_munchie = \n"
2779 "{\n"
2780 " ['IP']='%s',\n"
2781 " ['name']='%s',\n"
2782 " ['level']='%s',\n"
2783 " ['passwordSalt']='%s',\n"
2784 " ['passwordHash']='%s',\n"
2785 " ['salt']='%s',\n"
2786 " ['seshID']='%s',\n"
2787 " ['UUID']='%s',\n"
2788 " ['linky-hashish']='%s',\n"
2789 "}\n"
2790 "return toke_n_munchie\n",
2791 getStrH(Rd->headers, "REMOTE_ADDR"),
2792 getStrH(Rd->stuff, "name"),
2793 getStrH(Rd->stuff, "level"),
2794 getStrH(Rd->stuff, "passwordSalt"),
2795 getStrH(Rd->stuff, "passwordHash"),
2796 shs->salt,
2797 shs->seshID,
2798 getStrH(Rd->stuff, "UUID"),
2799 link
2800 );
2801 int fd = notstdio(xcreate_stdio(file, O_CREAT | O_WRONLY | O_TRUNC | O_CLOEXEC, S_IRUSR | S_IWUSR));
2802 size_t l = strlen(tnm);
2803
2804 if (s)
2805 I("Creating session %s.", file);
2806 else
2807 C("Updating session %s.", file); // I don't think updates can occur now.
2808 if (l != writeall(fd, tnm, l))
2809 {
2810 perror_msg("Writing %s", file);
2811 freeSesh(Rd, linky, TRUE);
2812 }
2813 // Set the mtime on the file.
2814 futimens(fd, shs->timeStamp);
2815 xclose(fd);
2816 free(file);
2817}
2818
2819static void createUser(reqData *Rd)
2820{
2821 char *file = xmprintf("%s/var/lib/users/%s.lua", getStrH(Rd->configs, "scRoot"), getStrH(Rd->stuff, "UUID"));
2822 char *tnm = xmprintf( "user = \n"
2823 "{\n"
2824 " ['name']='%s',\n"
2825// 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.
2826 " ['created']='%ld',\n"
2827 " ['email']='%s',\n"
2828 " ['title']='%s',\n"
2829 " ['level']='%d',\n"
2830 " ['flags']='%d',\n"
2831 " ['active']='%d',\n"
2832 " ['passwordSalt']='%s',\n"
2833 " ['passwordHash']='%s',\n"
2834 " ['UUID']='%s',\n"
2835 " ['DoB']='%s-%s',\n"
2836 " ['agree']='%s',\n"
2837 " ['adult']='%s',\n"
2838 " ['vouched']='%s',\n"
2839 "}\n"
2840 "return user\n",
2841 getStrH(Rd->stuff, "name"),
2842 (long) Rd->shs.timeStamp[1].tv_sec,
2843 getStrH(Rd->body, "email"),
2844 "newbie",
2845 -200,
2846 64,
2847 0,
2848 getStrH(Rd->stuff, "passwordSalt"),
2849 getStrH(Rd->stuff, "passwordHash"),
2850 getStrH(Rd->stuff, "UUID"),
2851 getStrH(Rd->body, "year"),
2852 getStrH(Rd->body, "month"),
2853 getStrH(Rd->body, "agree"),
2854 getStrH(Rd->body, "adult"),
2855 "off"
2856 );
2857
2858 struct stat st;
2859 int s = stat(file, &st);
2860
2861 int fd = notstdio(xcreate_stdio(file, O_CREAT | O_WRONLY | O_TRUNC | O_CLOEXEC, S_IRUSR | S_IWUSR));
2862 size_t l = strlen(tnm);
2863
2864 if (s)
2865 I("Creating user %s.", file);
2866 else
2867 C("Updating user %s.", file);
2868 if (l != writeall(fd, tnm, l))
2869 perror_msg("Writing %s", file);
2870 else
2871 {
2872 char *name = Rd->stuff->getstr(Rd->stuff, "name", true);
2873 char *nm = xmprintf("%s/var/lib/users/%s.lua", getStrH(Rd->configs, "scRoot"), qstrreplace("tr", name, " ", "_"));
2874
2875 free(file);
2876 file = xmprintf("%s.lua", getStrH(Rd->stuff, "UUID"));
2877 I("Symlinking %s to %s", file, nm);
2878 if (0 != symlink(file, nm))
2879 perror_msg("Symlinking %s to %s", file, nm);
2880 free(nm); free(name);
2881 }
2882 xclose(fd);
2883 free(file);
2884}
2885
2886
2887static void bitch(reqData *Rd, char *message, char *log)
2888{
2889 addStrL(Rd->errors, message);
2890 E("%s %s %s %s %s", getStrH(Rd->headers, "REMOTE_ADDR"), getStrH(Rd->stuff, "UUID"), getStrH(Rd->stuff, "name"), message, log);
2891}
2892
2893/* "A token cookie that references a non-existent session, its value should be replaced immediately to prevent session fixation."
2894https://owasp.org/www-community/attacks/Session_fixation
2895 Which describes the problem, but offers no solution.
2896 See https://stackoverflow.com/questions/549/the-definitive-guide-to-form-based-website-authentication?rq=1.
2897I think this means send a new cookie.
2898 I clear out the cookies and send blank ones with -1 maxAge, so they should get deleted.
2899*/
2900static void bitchSession(reqData *Rd, char *message, char *log)
2901{
2902 addStrL(Rd->errors, message);
2903 C("%s %s %s %s %s", getStrH(Rd->headers, "REMOTE_ADDR"), getStrH(Rd->stuff, "UUID"), getStrH(Rd->stuff, "name"), message, log);
2904 Rd->vegOut = TRUE;
2905}
2906static sesh *newSesh(reqData *Rd, boolean linky)
2907{
2908 unsigned char *md5hash = xzalloc(17);
2909 char *toke_n_munchie, *munchie, *hashish, *t0, *t1;
2910 char uuid[37];
2911 uuid_t binuuid;
2912 sesh *ret = &Rd->shs;
2913
2914W("New sesh");
2915 if (linky)
2916 {
2917 Rd->lnk = xzalloc(sizeof(sesh));
2918 ret = Rd->lnk;
2919 }
2920
2921 char buf[128]; // 512 bits.
2922 int numBytes = getrandom((void *)buf, sizeof(buf), GRND_NONBLOCK);
2923
2924 // NOTE that getrandom() returns random bytes, which may include '\0'.
2925 if (-1 == numBytes)
2926 {
2927 perror_msg("Unable to generate a suitable random number.");
2928 // EAGAIN - not enough entropy, try again.
2929 // EINTR - signal handler interrupted it, try again.
2930 }
2931 else
2932 {
2933 qstrcpy(ret->salt, sizeof(ret->salt), qhex_encode(buf, sizeof(buf)));
2934//D("salt %s", ret->salt);
2935 numBytes = getrandom((void *)buf, sizeof(buf), GRND_NONBLOCK);
2936 if (-1 == numBytes)
2937 perror_msg("Unable to generate a suitable random number.");
2938 else
2939 {
2940 qstrcpy(ret->seshID, sizeof(ret->seshID), qhex_encode(buf, sizeof(buf)));
2941//D("seshID %s", ret->seshID);
2942
2943 ret->timeStamp[0].tv_nsec = UTIME_OMIT;
2944 ret->timeStamp[0].tv_sec = UTIME_OMIT;
2945 if (-1 == clock_gettime(CLOCK_REALTIME, &ret->timeStamp[1]))
2946 perror_msg("Unable to get the time.");
2947 else
2948 {
2949 // 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.
2950 t0 = xmprintf("%s%ld.%ld", ret->seshID, (long) ret->timeStamp[1].tv_sec, ret->timeStamp[1].tv_nsec);
2951 qstrcpy(ret->sesh, sizeof(ret->sesh), t0);
2952//D("sesh %s", ret->sesh);
2953 t1 = myHMAC(t0, FALSE);
2954 free(t0);
2955 munchie = xmprintf("%s%ld.%ld", t1, (long) ret->timeStamp[1].tv_sec, ret->timeStamp[1].tv_nsec);
2956 free(t1);
2957 qstrcpy(ret->munchie, sizeof(ret->munchie), munchie);
2958//D("munchie %s", ret->munchie);
2959 t0 = xmprintf("%s%s", getStrH(Rd->stuff, "UUID"), munchie);
2960 toke_n_munchie = myHMAC(t0, FALSE);
2961 free(t0);
2962 qstrcpy(ret->toke_n_munchie, sizeof(ret->toke_n_munchie), toke_n_munchie);
2963//D("toke_n_munchie %s", ret->toke_n_munchie);
2964 hashish = myHMACkey(ret->salt, toke_n_munchie, FALSE);
2965 qstrcpy(ret->hashish, sizeof(ret->hashish), hashish);
2966//D("hashish %s", ret->hashish);
2967 t0 = myHMACkey(getStrH(Rd->configs, "pepper"), hashish, TRUE);
2968 qstrcpy(ret->leaf, sizeof(ret->leaf), t0);
2969//D("leaf %s", ret->leaf);
2970 free(t0);
2971 ret->isLinky = linky;
2972 setToken_n_munchie(Rd, linky);
2973 }
2974 }
2975 }
2976
2977 free(md5hash);
2978 return ret;
2979}
2980
2981char *checkLinky(reqData *Rd)
2982{
2983 char *ret = xstrdup(""), *t0 = getStrH(Rd->stuff, "linky-hashish");
2984
2985 if ('\0' != t0[0])
2986 {
2987 char *t1 = qurl_encode(t0, strlen(t0));
2988 free(ret);
2989 ret = xmprintf("<p><font color='red'><b>You have an email waiting with a linky in it <a href='https://%s%s?hashish=%s'>%s</a>.</b></font></p>\n",
2990 Rd->Host, Rd->RUri, t1, t0);
2991 free(t1);
2992 }
2993 return ret;
2994}
2995
2996
2997boolean prevalidate(qhashtbl_t *data, char *name)
2998{
2999 if ('\0' != getStrH(data, name)[0])
3000 {
3001 I("Already validated %s.", name);
3002 return TRUE;
3003 }
3004 return FALSE;
3005}
3006
3007boolean badBoy(int ret, reqData *Rd, qhashtbl_t *data, char *name, char *value)
3008{
3009 if (NULL == value)
3010 value = getStrH(data, name);
3011
3012 if (0 != ret)
3013 {
3014 char *t = xmprintf("BAD - %s", name);
3015
3016 Rd->stuff->putstr(Rd->stuff, t, value);
3017 Rd->stuff->remove(data, name);
3018 free(t);
3019 return TRUE;
3020 }
3021 data->putstr(data, name, value);
3022 return FALSE;
3023}
3024
3025char *months[] =
3026{
3027 "january",
3028 "february",
3029 "march",
3030 "april",
3031 "may",
3032 "june",
3033 "july",
3034 "august",
3035 "september",
3036 "october",
3037 "november",
3038 "december"
3039};
3040
3041
3042
3043int LuaToHash(reqData *Rd, char *file, char *var, qhashtbl_t *tnm, int ret, struct stat *st, struct timespec *now, char *type)
3044{
3045 struct timespec then;
3046
3047 if (-1 == clock_gettime(CLOCK_REALTIME, &then))
3048 perror_msg("Unable to get the time.");
3049 I("Reading %s file %s", type, file);
3050 if (0 != stat(file, st))
3051 {
3052 bitchSession(Rd, "No such thing.", "No file.");
3053 perror_msg("Unable to stat %s", file);
3054 ret++;
3055 }
3056 else
3057 {
3058 int status = luaL_loadfile(Rd->L, file), result;
3059
3060 if (status)
3061 {
3062 bitchSession(Rd, "No such thing.", "Can't load file.");
3063 E("Couldn't load file: %s", lua_tostring(Rd->L, -1));
3064 ret++;
3065 }
3066 else
3067 {
3068 result = lua_pcall(Rd->L, 0, LUA_MULTRET, 0);
3069
3070 if (result)
3071 {
3072 bitchSession(Rd, "Broken thing.", "Can't run file.");
3073 E("Failed to run script: %s", lua_tostring(Rd->L, -1));
3074 ret++;
3075 }
3076 else
3077 {
3078 lua_getglobal(Rd->L, var);
3079 lua_pushnil(Rd->L);
3080
3081 while(lua_next(Rd->L, -2) != 0)
3082 {
3083 char *n = (char *) lua_tostring(Rd->L, -2);
3084
3085 if (lua_isstring(Rd->L, -1))
3086 {
3087 tnm->putstr(tnm, n, (char *) lua_tostring(Rd->L, -1));
3088//D("Reading %s %s", n, getStrH(tnm, n));
3089 }
3090 else
3091 {
3092 char *v = (char *) lua_tostring(Rd->L, -1);
3093 W("Unknown Lua variable type for %s = %s", n, v);
3094 }
3095 lua_pop(Rd->L, 1);
3096 }
3097
3098 if (-1 == clock_gettime(CLOCK_REALTIME, now))
3099 perror_msg("Unable to get the time.");
3100 double n = (now->tv_sec * 1000000000.0) + now->tv_nsec;
3101 double t = (then.tv_sec * 1000000000.0) + then.tv_nsec;
3102 T("Reading %s file took %lf seconds", type, (n - t) / 1000000000.0);
3103 }
3104 }
3105 }
3106
3107 return ret;
3108}
3109
3110static int validateSesh(reqData *Rd, qhashtbl_t *data)
3111{
3112 int ret = 0;
3113 boolean linky = FALSE;
3114
3115 if ('\0' != Rd->shs.leaf[0])
3116 {
3117 I("Already validated session.");
3118 return ret;
3119 }
3120
3121 I("Validating session.");
3122
3123 char *toke_n_munchie = "", *munchie = "", *hashish = "",
3124 *leaf, *timeStamp = "", *seshion = "", *seshID = "",
3125 *t0, *t1;
3126
3127 // In this case the session stuff has to come from specific places.
3128 hashish = getStrH(Rd->queries, "hashish");
3129//D("O hashish %s", hashish);
3130 if ('\0' != hashish[0])
3131 linky = TRUE;
3132 else
3133 {
3134 toke_n_munchie = getStrH(Rd->cookies, "toke_n_munchie");
3135 munchie = getStrH(Rd->body, "munchie");
3136 hashish = getStrH(Rd->cookies, "hashish");
3137 if (('\0' == toke_n_munchie[0]) || (('\0' == hashish[0])))
3138 {
3139 bitchSession(Rd, "Invalid session.", "No or blank hashish or toke_n_munchie.");
3140 ret++;
3141 }
3142 }
3143
3144//D("O hashish %s", hashish);
3145//D("O toke_n_munchie %s", toke_n_munchie);
3146//D("O munchie %s", munchie);
3147 if (0 == ret)
3148 {
3149 struct stat st;
3150 struct timespec now;
3151
3152 leaf = myHMACkey(getStrH(Rd->configs, "pepper"), hashish, TRUE);
3153//D("leaf %s", leaf);
3154 if (linky)
3155 t0 = xmprintf("%s/caches/sessions/%s.linky", getStrH(Rd->configs, "scRoot"), leaf);
3156 else
3157 t0 = xmprintf("%s/caches/sessions/%s.lua", getStrH(Rd->configs, "scRoot"), leaf);
3158
3159 qhashtbl_t *tnm = qhashtbl(0, 0);
3160 ret = LuaToHash(Rd, t0, "toke_n_munchie", tnm, ret, &st, &now, "session");
3161 free(t0);
3162
3163 if (0 == ret)
3164 {
3165 {
3166 {
3167 // This is apparently controversial, I added it coz some of the various security docs suggested it's a good idea.
3168 // https://security.stackexchange.com/questions/139952/why-arent-sessions-exclusive-to-an-ip-address?rq=1
3169 // Includes various reasons why it's bad.
3170 // Another good reason why it is bad, TOR.
3171 // So should make this a user option, like Mantis does.
3172 if (strcmp(getStrH(Rd->headers, "REMOTE_ADDR"), getStrH(tnm, "IP")) != 0)
3173 {
3174 bitchSession(Rd, "Wrong IP for session.", "Session IP doesn't match.");
3175 ret++;
3176 }
3177 else
3178 {
3179 timeStamp = xmprintf("%ld.%ld", (long) st.st_mtim.tv_sec, st.st_mtim.tv_nsec);
3180//D("timeStamp %s", timeStamp);
3181 seshion = xmprintf("%s%s", tnm->getstr(tnm, "seshID", false), timeStamp);
3182//D("sesh %s", seshion);
3183 t0 = myHMAC(seshion, FALSE);
3184 munchie = xmprintf("%s%s", t0, timeStamp);
3185//D("munchie %s", munchie);
3186 free(t0);
3187 t1 = getStrH(Rd->body, "munchie");
3188 if ('\0' != t1[0])
3189 {
3190 if (strcmp(t1, munchie) != 0)
3191 {
3192 bitchSession(Rd, "Wrong munchie for session.", "HMAC(seshID + timeStamp) != munchie");
3193 ret++;
3194 }
3195 else
3196 {
3197 t0 = xmprintf("%s%s", getStrH(tnm, "UUID"), munchie);
3198 t1 = myHMAC(t0, FALSE);
3199 free(t0);
3200
3201//D("toke_n_munchie %s", t1);
3202 if (strcmp(t1, toke_n_munchie) != 0)
3203 {
3204 bitchSession(Rd, "Wrong toke_n_munchie for session.", "HMAC(UUID + munchie) != toke_n_munchie");
3205 ret++;
3206 }
3207 free(t1);
3208 }
3209 }
3210
3211 if (0 == ret)
3212 {
3213 if (linky)
3214 {
3215 t0 = xmprintf("%s%s", getStrH(tnm, "UUID"), munchie);
3216 t1 = myHMAC(t0, FALSE);
3217 free(t0);
3218 toke_n_munchie = t1;
3219//D("toke_n_munchie %s", t1);
3220 }
3221 t1 = myHMACkey(getStrH(tnm, "salt"), toke_n_munchie, FALSE);
3222//D("hashish %s", t1);
3223 if (strcmp(t1, hashish) != 0)
3224 {
3225 bitchSession(Rd, "Wrong hashish for session.", "HMAC(toke_n_munchie + salt) != hashish");
3226 ret++;
3227 }
3228
3229 if (now.tv_sec > st.st_mtim.tv_sec + idleTimeOut)
3230 {
3231 W("Session idled out.");
3232 Rd->vegOut = TRUE;
3233 }
3234 else
3235 {
3236 if (now.tv_sec > st.st_mtim.tv_sec + seshTimeOut)
3237 {
3238 W("Session timed out.");
3239 Rd->vegOut = TRUE;
3240 }
3241 else
3242 {
3243W("Validated session.");
3244 sesh *shs = &Rd->shs;
3245
3246 qstrcpy(shs->leaf, sizeof(shs->leaf), leaf);
3247 if (linky)
3248 {
3249W("Validated session linky.");
3250 addStrL(Rd->messages, "Congratulations, you have validated your new account. Now you can log onto the web site.");
3251 addStrL(Rd->messages, "NOTE - you wont be able to log onto the grid until your new account has been approved.");
3252 Rd->lnk = xzalloc(sizeof(sesh));
3253 qstrcpy(Rd->lnk->leaf, sizeof(Rd->lnk->leaf), leaf);
3254 Rd->chillOut = TRUE;
3255 freeSesh(Rd, linky, FALSE);
3256 qstrcpy(Rd->lnk->leaf, sizeof(Rd->lnk->leaf), "");
3257 Rd->func = (pageBuildFunction) loginPage;
3258 Rd->doit = "logout";
3259// TODO - we might want to delete their old .lua session as well. Maybe? Don't think we have any suitable codes to find it.
3260 }
3261 else
3262 {
3263 qstrcpy(shs->sesh, sizeof(shs->sesh), seshion);
3264 qstrcpy(shs->toke_n_munchie, sizeof(shs->toke_n_munchie), toke_n_munchie);
3265 qstrcpy(shs->hashish, sizeof(shs->hashish), hashish);
3266 qstrcpy(shs->munchie, sizeof(shs->munchie), munchie);
3267 qstrcpy(shs->salt, sizeof(shs->salt), tnm->getstr(tnm, "salt", false));
3268 qstrcpy(shs->seshID, sizeof(shs->seshID), tnm->getstr(tnm, "seshID", false));
3269 shs->timeStamp[0].tv_nsec = UTIME_OMIT;
3270 shs->timeStamp[0].tv_sec = UTIME_OMIT;
3271 memcpy(&shs->timeStamp[1], &st.st_mtim, sizeof(struct timespec));
3272 t0 = tnm->getstr(tnm, "linky-hashish", false);
3273 if (NULL != t0)
3274 Rd->stuff->putstr(Rd->stuff, "linky-hashish", t0);
3275 }
3276 }
3277 Rd->stuff->putstr(Rd->stuff, "name", tnm->getstr(tnm, "name", true));
3278 Rd->stuff->putstr(Rd->stuff, "UUID", tnm->getstr(tnm, "UUID", true));
3279 Rd->stuff->putstr(Rd->stuff, "level", tnm->getstr(tnm, "level", true));
3280 Rd->stuff->putstr(Rd->stuff, "passwordSalt", tnm->getstr(tnm, "passwordSalt", true));
3281 Rd->stuff->putstr(Rd->stuff, "passwordHash", tnm->getstr(tnm, "passwordHash", true));
3282 Rd->database->putstr(Rd->database, "UserAccounts.PrincipalID", tnm->getstr(tnm, "UUID", true));
3283 }
3284 }
3285
3286 }
3287 }
3288 }
3289 }
3290 }
3291
3292 return ret;
3293}
3294
3295static int validateDoB(reqData *Rd, qhashtbl_t *data)
3296{
3297 int ret = 0, i;
3298 char *t;
3299
3300 if (prevalidate(Rd->stuff, "year")) return ret;
3301 if (prevalidate(Rd->stuff, "month")) return ret;
3302
3303 I("Validating DoB.");
3304 t = getStrH(data, "year");
3305 if ((NULL == t) || ('\0' == t[0]))
3306 {
3307 bitch(Rd, "Please supply a year of birth.", "None supplied.");
3308 ret++;
3309 }
3310 else
3311 {
3312 i = atoi(t);
3313 if ((1900 > i) || (i > 2020))
3314 {
3315 bitch(Rd, "Please supply a year of birth.", "Out of range.");
3316 ret++;
3317 }
3318 }
3319
3320 t = getStrH(data, "month");
3321 if ((NULL == t) || ('\0' == t[0]))
3322 {
3323 bitch(Rd, "Please supply a month of birth.", "None supplied.");
3324 ret++;
3325 }
3326 else
3327 {
3328 for (i = 0; i < 12; i++)
3329 {
3330 if (strcmp(months[i], t) == 0)
3331 break;
3332 }
3333 if (12 == i)
3334 {
3335 bitch(Rd, "Please supply a month of birth.", "Out of range");
3336 ret++;
3337 }
3338 }
3339
3340 badBoy(ret, Rd, data, "month", NULL);
3341 badBoy(ret, Rd, data, "year", NULL);
3342 return ret;
3343}
3344
3345static int validateEmail(reqData *Rd, qhashtbl_t *data)
3346{
3347 int ret = 0;
3348 char *email = getStrH(data, "email");
3349 char *emayl = getStrH(data, "emayl");
3350
3351 if ((strcmp("create", Rd->doit) != 0) && (strcmp("update", Rd->doit) != 0))
3352 return ret;
3353
3354 if (prevalidate(Rd->stuff, "email")) return ret;
3355 if (prevalidate(Rd->stuff, "emayl")) return ret;
3356
3357 I("Validating email.");
3358 if ((NULL == email) || (NULL == emayl) || ('\0' == email[0]) || ('\0' == emayl[0]))
3359 {
3360 bitch(Rd, "Please supply an email address.", "None supplied.");
3361 ret++;
3362 }
3363 else if (strcmp(email, emayl) != 0)
3364 {
3365 bitch(Rd, "Email addresses are not the same.", "");
3366 ret++;
3367 }
3368 else if (!qstr_is_email(email))
3369 {
3370 bitch(Rd, "Please supply a proper email address.", "Failed qstr_is_email()");
3371 ret++;
3372 }
3373 else
3374 {
3375// TODO - do other email checks - does the domain exist, ..
3376 }
3377
3378 badBoy(ret, Rd, data, "email", email);
3379 badBoy(ret, Rd, data, "emayl", emayl);
3380 return ret;
3381}
3382
3383static int validateLegal(reqData *Rd, qhashtbl_t *data)
3384{
3385 int ret = 0;
3386 char *t;
3387
3388 if (prevalidate(Rd->stuff, "adult")) return ret;
3389 if (prevalidate(Rd->stuff, "agree")) return ret;
3390
3391
3392 I("Validating legal.");
3393 t = getStrH(data, "adult");
3394 if ((NULL == t) || (strcmp("on", t) != 0))
3395 {
3396 bitch(Rd, "You must be an adult to enter this world.", "");
3397 ret++;
3398 }
3399 t = getStrH(data, "agree");
3400 if ((NULL == t) || (strcmp("on", t) != 0))
3401 {
3402 bitch(Rd, "You must agree to the Terms & Conditions of Use.", "");
3403 ret++;
3404 }
3405
3406 badBoy(ret, Rd, data, "adult", NULL);
3407 badBoy(ret, Rd, data, "agree", NULL);
3408 return ret;
3409}
3410
3411static int validateName(reqData *Rd, qhashtbl_t *data)
3412{
3413 boolean login = strcmp("login", Rd->doit) == 0;
3414 int ret = 0;
3415 unsigned char *name = data->getstr(data, "name", true); // We have to be unsigned coz of isalnum().
3416 char *where = NULL;
3417
3418 if (strcmp("logout", Rd->doit) == 0)
3419 return ret;
3420
3421 if (prevalidate(Rd->database, "UserAccounts.UserLevel")) return ret;
3422
3423 I("Validating name.");
3424 if ((NULL == name) || ('\0' == name[0]))
3425 {
3426 bitch(Rd, "Please supply an account name.", "None supplied.");
3427 ret++;
3428 }
3429 else
3430 {
3431 int l0 = strlen(name), l1;
3432
3433 if (0 == l0)
3434 {
3435 bitch(Rd, "Please supply an account name.", "Name is empty.");
3436 ret++;
3437 }
3438 else
3439 {
3440 int i;
3441 unsigned char *s = NULL;
3442
3443 for (i = 0; i < l0; i++)
3444 {
3445 if (isalnum(name[i]) == 0)
3446 {
3447
3448 if ((' ' == name[i] && (NULL == s)))
3449 {
3450 s = &name[i];
3451 *s++ = '\0';
3452 l1 = strlen(s);
3453
3454 // Apparently names are not case sensitive on login, but stored with case in the database.
3455 // I confirmed that, can log in no matter what case you use.
3456 // Seems to be good security for names to be cose insensitive.
3457 // UserAccounts FirstName and LastName fields are both varchar(64) utf8_general_ci.
3458 // The MySQL docs say that the "_ci" bit means comparisons will be case insensitive. So that should work fine.
3459
3460 // 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.
3461 // The userinfo table seems to be obsolete.
3462 // Singularity at least limits the total name to 64.
3463 // I can't find any limitations on characters allowed, but I only ever see letters and digits used. Case is stored, but not significant.
3464 // OpenSims "create user" console command doesn't sanitize it at all, even crashing on some names.
3465 if (0 == l1)
3466 {
3467 bitch(Rd, "Account names have to be two words.", "");
3468 ret++;
3469 }
3470 if ((31 < i) || (31 < l1))
3471 {
3472 bitch(Rd, "First and last names are limited to 31 letters each.", "");
3473 ret++;
3474 }
3475 }
3476 else
3477 {
3478 bitch(Rd, "First and last names are limited to letters and digits.", "");
3479 ret++;
3480 break;
3481 }
3482 }
3483 }
3484
3485 struct stat st;
3486 struct timespec now;
3487 qhashtbl_t *tnm = qhashtbl(0, 0);
3488 int rt = 0;
3489
3490 if (s) {s--; *s = '_'; s++;}
3491 where = xmprintf("%s/var/lib/users/%s.lua", getStrH(Rd->configs, "scRoot"), name);
3492 rt = LuaToHash(Rd, where, "user", tnm, ret, &st, &now, "user");
3493 if (s) {s--; *s = '\0'; s++;}
3494 free(where);
3495
3496 if (0 != rt)
3497 {
3498 bitch(Rd, "Login failed.", "Could not read user Lua file.");
3499 ret += rt;
3500 }
3501 else
3502 {
3503 Rd->database->putstr(Rd->database, "UserAccounts.FirstName", name);
3504 Rd->database->putstr(Rd->database, "UserAccounts.LastName", s);
3505 Rd->database->putstr(Rd->database, "UserAccounts.Email", getStrH(tnm, "email"));
3506 Rd->database->putstr(Rd->database, "UserAccounts.Created", getStrH(tnm, "created"));
3507 Rd->database->putstr(Rd->database, "UserAccounts.PrincipleID", getStrH(tnm, "UUID"));
3508 Rd->database->putstr(Rd->database, "UserAccounts.UserLevel", getStrH(tnm, "level"));
3509 Rd->database->putstr(Rd->database, "UserAccounts.UserFlags", getStrH(tnm, "flags"));
3510 Rd->database->putstr(Rd->database, "UserAccounts.UserTitle", getStrH(tnm, "title"));
3511 Rd->database->putstr(Rd->database, "UserAccounts.active", getStrH(tnm, "active"));
3512 Rd->database->putstr(Rd->database, "auth.passwordSalt", getStrH(tnm, "passwordSalt"));
3513 Rd->database->putstr(Rd->database, "auth.passwordHash", getStrH(tnm, "passwordHash"));
3514
3515 Rd->stuff->putstr(Rd->stuff, "UUID", xstrdup(getStrH(Rd->database, "UserAccounts.PrincipalID")));
3516 Rd->stuff->putstr(Rd->stuff, "level", xstrdup(getStrH(Rd->database, "UserAccounts.Userlevel")));
3517 if (s) {s--; *s = ' '; s++;}
3518 Rd->stuff->putstr(Rd->stuff, "name", xstrdup(name));
3519 if (s) {s--; *s = '\0'; s++;}
3520 }
3521
3522
3523 static dbRequest *acnts = NULL;
3524 if (NULL == acnts)
3525 {
3526 static char *szi[] = {"FirstName", "LastName", NULL};
3527 static char *szo[] = {NULL};
3528 acnts = xzalloc(sizeof(dbRequest));
3529 acnts->db = Rd->db;
3530 acnts->table = "UserAccounts";
3531 acnts->inParams = szi;
3532 acnts->outParams = szo;
3533 acnts->where = "FirstName=? and LastName=?";
3534 }
3535 dbDoSomething(acnts, FALSE, name, s);
3536 rowData *rows = acnts->rows;
3537 if (rows)
3538 {
3539 int i = rows->rows->size(rows->rows);
3540
3541 if (login)
3542 {
3543if (rt)
3544{
3545 if (i == 0)
3546 {
3547 bitch(Rd, "Login failed.", "No UserAccounts record with that name.");
3548 ret++;
3549 }
3550 else
3551 {
3552 if (i != 1)
3553 {
3554 bitch(Rd, "Login failed.", "More than one UserAccounts record with that name.");
3555 ret++;
3556 }
3557 else
3558 {
3559 dbPull(Rd, "UserAccounts", rows);
3560 Rd->stuff->putstr(Rd->stuff, "UUID", xstrdup(getStrH(Rd->database, "UserAccounts.PrincipalID")));
3561 Rd->stuff->putstr(Rd->stuff, "level", xstrdup(getStrH(Rd->database, "UserAccounts.Userlevel")));
3562 if (s) {s--; *s = ' '; s++;}
3563 Rd->stuff->putstr(Rd->stuff, "name", xstrdup(name));
3564 }
3565 }
3566}
3567 }
3568 else if (strcmp("create", Rd->doit) == 0)
3569 {
3570 if (i != 0)
3571 {
3572 bitch(Rd, "Pick a different name.", "An existing UserAccounts record matched that name.");
3573 ret++;
3574 }
3575 else
3576 {
3577// TODO - compare first, last, and fullname against god names, complain and fail if there's a match.
3578 {
3579 // Generate a UUID, check it isn't already being used.
3580 char uuid[37];
3581 uuid_t binuuid;
3582 my_ulonglong users = 0;
3583
3584 do // UserAccounts.PrincipalID is a unique primary index anyway, but we want the user creation process to be a little on the slow side.
3585 {
3586 uuid_generate_random(binuuid);
3587 uuid_unparse_lower(binuuid, uuid);
3588 where = xmprintf("UserAccounts.PrincipalID = '%s'", uuid);
3589 D("Trying new UUID %s.", where);
3590 users = dbCount(Rd->db, "UserAccounts", where);
3591 free(where);
3592 } while (users != 0);
3593// TODO - perhaps create a place holder UserAccounts record? Then we'll have to deal with deleting them later.
3594
3595 Rd->stuff->putstr(Rd->stuff, "UUID", xstrdup(uuid));
3596 Rd->stuff->putstr(Rd->stuff, "level", xstrdup("-200"));
3597 Rd->database->putstr(Rd->database, "UserAccounts.PrincipalID", xstrdup(uuid));
3598 Rd->database->putstr(Rd->database, "UserAccounts.Userlevel", xstrdup("-200"));
3599 Rd->database->putstr(Rd->database, "UserAccounts.firstName", xstrdup(name));
3600 Rd->database->putstr(Rd->database, "UserAccounts.lastName", xstrdup(s));
3601 if (s) {s--; *s = ' '; s++;}
3602 Rd->stuff->putstr(Rd->stuff, "name", xstrdup(name));
3603 }
3604 }
3605 }
3606 free(rows->rows);
3607 free(rows->fieldNames);
3608 free(rows);
3609 }
3610
3611 if (s) {s--; *s = ' '; s++;}
3612 }
3613 }
3614
3615 badBoy(ret, Rd, data, "name", NULL);
3616 return ret;
3617}
3618
3619static int validatePassword(reqData *Rd, qhashtbl_t *data)
3620{
3621 boolean login = strcmp("login", Rd->doit) == 0;
3622 boolean create = strcmp("create", Rd->doit) == 0;
3623 int ret = 0;
3624 char *password = getStrH(data, "password");
3625 char *psswrd = getStrH(data, "psswrd");
3626 char *psswrdH = getStrH(Rd->stuff, "passwordHash");
3627 char *psswrdS = getStrH(Rd->stuff, "passwordSalt");
3628
3629 if (prevalidate(Rd->database, "auth.passwordSalt")) return ret;
3630
3631 I("Validating password.");
3632 if (login)
3633 {
3634 char *UUID = getStrH(Rd->database, "UserAccounts.PrincipalID");
3635 static dbRequest *auth = NULL;
3636 if (NULL == auth)
3637 {
3638 static char *szi[] = {"UUID", NULL};
3639 static char *szo[] = {"passwordSalt", "passwordHash", NULL};
3640 auth = xzalloc(sizeof(dbRequest));
3641 auth->db = Rd->db;
3642 auth->table = "auth";
3643 auth->inParams = szi;
3644 auth->outParams = szo;
3645 auth->where = "UUID=?";
3646 }
3647 dbDoSomething(auth, FALSE, UUID);
3648 rowData *rows = auth->rows;
3649 if (rows)
3650 {
3651 int i = rows->rows->size(rows->rows);
3652
3653 if (i != 1)
3654 {
3655 bitch(Rd, "Login failed.", "Wrong number of auth records.");
3656 ret++;
3657 }
3658 else
3659 {
3660 qhashtbl_t *me = rows->rows->popfirst(rows->rows, NULL);
3661 unsigned char md5hash[16];
3662
3663 if (!qhashmd5((void *) password, strlen(password), md5hash))
3664 {
3665 bitch(Rd, "Login failed, internal error.", "Login - qhashmd5(password) failed.");
3666 ret++;
3667 }
3668 else
3669 {
3670 Rd->stuff->putstr(Rd->stuff, "passwordSalt", getStrH(me, "passwordSalt"));
3671
3672 char *md5ascii = qhex_encode(md5hash, 16);
3673 char *where = xmprintf("%s:%s", md5ascii, getStrH(me, "passwordSalt"));
3674 if (!qhashmd5((void *) where, strlen(where), md5hash))
3675 {
3676 bitch(Rd, "Login failed, internal error.", "Login - qhashmd5(passwordSalt) failed.");
3677 ret++;
3678 }
3679 else
3680 {
3681 free(md5ascii);
3682 md5ascii = qhex_encode(md5hash, 16);
3683 if (strcmp(md5ascii, getStrH(me, "passwordHash")) != 0)
3684 {
3685 bitch(Rd, "Login failed.", "passwordHash doesn't match");
3686 ret++;
3687 }
3688 Rd->stuff->putstr(Rd->stuff, "passwordHash", md5ascii);
3689 free(md5ascii);
3690 }
3691 free(where);
3692 }
3693 }
3694 }
3695 }
3696 else if (create)
3697 {
3698 if ((NULL == password) || ('\0' == password[0]))
3699 {
3700 bitch(Rd, "Please supply a password.", "Password empty or missing.");
3701 ret++;
3702 }
3703 else
3704 {
3705 // https://stackoverflow.com/questions/246930/is-there-any-difference-between-a-guid-and-a-uuid
3706 // Has a great discussion.
3707 // http://tools.ietf.org/html/rfc4122
3708 // https://en.wikipedia.org/wiki/Universally_unique_identifier
3709 // Useful stuff about versions and variants, a quick look says OpenSim is using version 4 (random), variant 1.
3710 // Note versions 3 and 5 are hash based, like I wanted for SledjHamr. B-)
3711 // 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.
3712 // 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).
3713
3714 // Calculate passwordSalt and passwordHash. From Opensim -
3715 // passwordSalt = Util.Md5Hash(UUID.Random().ToString())
3716 // passwdHash = Util.Md5Hash(Util.Md5Hash(password) + ":" + passwordSalt)
3717 unsigned char *md5hash = xzalloc(17);
3718 char *salt, *hash;
3719 char uuid[37];
3720 uuid_t binuuid;
3721
3722 uuid_generate_random(binuuid);
3723 uuid_unparse_lower(binuuid, uuid);
3724 if (!qhashmd5((void *) uuid, strlen(uuid), md5hash))
3725 {
3726 bitch(Rd, "Internal session error.", "Create - qhashmd5(new uuid) failed.");
3727 ret++;
3728 }
3729 else
3730 {
3731 salt = qhex_encode(md5hash, 16);
3732 Rd->stuff->putstr(Rd->stuff, "passwordSalt", salt);
3733 if (!qhashmd5((void *) password, strlen(password), md5hash))
3734 {
3735 bitch(Rd, "Internal session error.", "Create - qhashmd5(password) failed.");
3736 ret++;
3737 }
3738 else
3739 {
3740 salt = qhex_encode(md5hash, 16);
3741 hash = xmprintf("%s:%s", salt, getStrH(Rd->stuff, "passwordSalt"));
3742 if (!qhashmd5((void *) hash, strlen(hash), md5hash))
3743 {
3744 bitch(Rd, "Internal session error.", "Create - qhashmd5(passwordSalt) failed.");
3745 ret++;
3746 }
3747 else
3748 {
3749 hash = qhex_encode(md5hash, 16);
3750 Rd->stuff->putstr(Rd->stuff, "passwordHash", hash);
3751 Rd->chillOut = TRUE;
3752 }
3753 }
3754 }
3755 }
3756 }
3757 else if (strcmp("confirm", Rd->doit) == 0)
3758 {
3759 if ((NULL == password) || ('\0' == password[0]))
3760 {
3761 bitch(Rd, "Please supply a password.", "Need two passwords.");
3762 ret++;
3763 }
3764 else
3765 {
3766 unsigned char *md5hash = xzalloc(17);
3767 char *pswd, *hash;
3768 char *Osalt = getStrH(Rd->stuff, "passwordSalt"), *Ohash = getStrH(Rd->stuff, "passwordHash");
3769
3770 if (!qhashmd5((void *) password, strlen(password), md5hash))
3771 {
3772 bitch(Rd, "Internal session error.", "Confirm - qhashmd5(password) failed.");
3773 ret++;
3774 }
3775 else
3776 {
3777 pswd = qhex_encode(md5hash, 16);
3778 hash = xmprintf("%s:%s", pswd, Osalt);
3779
3780 if (!qhashmd5((void *) hash, strlen(hash), md5hash))
3781 {
3782 bitch(Rd, "Internal session error.", "Confirm - qhashmd5(passwordSalt) failed.");
3783 ret++;
3784 }
3785 else
3786 {
3787 hash = qhex_encode(md5hash, 16);
3788 if (strcmp(hash, Ohash) != 0)
3789 {
3790 bitch(Rd, "Passwords are not the same.", "");
3791 ret++;
3792 }
3793 }
3794 }
3795 }
3796 }
3797
3798// 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.
3799// And try to find code for dealing with security enclaves, encrypted memory, and such.
3800// NOTE - thes get giltered through what ever web server is being used, and might leak there.
3801 // explicit_bzero() is the magic to properly wipe things, and it exists, but the damn thing manages to hide itself.
3802 // So gotta make sure it's actually used, to avoid the compiler optimizing bzero() away.
3803// explicit_bzero(password, strlen(password));
3804// explicit_bzero(psswrd, strlen(psswrd));
3805 bzero(password, strlen(password));
3806 bzero(psswrd, strlen(psswrd));
3807 if (login)
3808 D("User logged in with %s or %s.", password, psswrd);
3809 else
3810 D("Account created with %s or %s.", password, psswrd);
3811
3812 return ret;
3813}
3814
3815
3816static int validateUUID(reqData *Rd, qhashtbl_t *data)
3817{
3818 int ret = 0, i;
3819 char uuid[37], *t;
3820 rowData *rows = NULL;
3821 static dbRequest *uuids = NULL;
3822 if (NULL == uuids)
3823 {
3824 static char *szi[] = {"PrincipalID", NULL};
3825 static char *szo[] = {NULL};
3826 uuids = xzalloc(sizeof(dbRequest));
3827 uuids->db = Rd->db;
3828 uuids->table = "UserAccounts";
3829 uuids->inParams = szi;
3830 uuids->outParams = szo;
3831 uuids->where = "PrincipalID=?";
3832 }
3833
3834 I("Validating UUID.");
3835 if (strcmp("create", Rd->doit) == 0)
3836 {
3837 // Generate a UUID, check it isn't already being used, and totally ignore whatever UUID is in body.
3838 uuid_t binuuid;
3839
3840 if (prevalidate(Rd->stuff, "UUID")) return ret;
3841
3842 do // UserAccounts.PrincipalID is a unique primary index anyway, but we want the user creation process to be a little on the slow side.
3843 {
3844 uuid_generate_random(binuuid);
3845 uuid_unparse_lower(binuuid, uuid);
3846
3847 D("Trying new UUID %s.", uuid);
3848// TODO - check the Lua user files as well.
3849 dbDoSomething(uuids, FALSE, uuid);
3850 rows = uuids->rows;
3851 i = 0;
3852 if (rows)
3853 i = rows->rows->size(rows->rows);
3854 else
3855 {
3856 bitch(Rd, "Internal error.", "Matching UUID record found in UserAccounts.");
3857 ret++;
3858 break;
3859 }
3860 } while (i != 0);
3861 if (0 == ret)
3862 {
3863 data->putstr(data, "UUID", xstrdup(uuid));
3864 data->putstr(data, "NEW - UUID", uuid);
3865 }
3866 rows = NULL;
3867 }
3868 else if ((strcmp("confirm", Rd->doit) == 0) || (strcmp("logout", Rd->doit) == 0))
3869 {
3870 t = getStrH(data, "UUID");
3871 if (36 != strlen(t))
3872 {
3873 bitch(Rd, "Internal error.", "UUID isn't long enough.");
3874 ret++;
3875 }
3876 else
3877 strcpy(uuid, t);
3878 }
3879 else
3880 {
3881 if ('\0' != getStrH(Rd->database, "UserAccounts.ScopeID")[0]) return ret;
3882
3883 t = getStrH(data, "UUID");
3884 if (36 != strlen(t))
3885 {
3886 bitch(Rd, "Internal error.", "UUID isn't long enough.");
3887 ret++;
3888 }
3889 else
3890 {
3891 strcpy(uuid, t);
3892 dbDoSomething(uuids, FALSE, uuid);
3893 rows = uuids->rows;
3894 if (rows)
3895 {
3896 if (1 != rows->rows->size(rows->rows))
3897 {
3898 bitch(Rd, "Internal error.", "No matching UUID record found in UserAccounts.");
3899 ret++;
3900 }
3901 }
3902 }
3903 }
3904
3905 if (!badBoy(ret, Rd, data, "UUID", uuid))
3906 {
3907 if (rows)
3908 {
3909 dbPull(Rd, "UserAccounts", rows);
3910 Rd->stuff->putstr(Rd->stuff, "level", xstrdup(getStrH(Rd->database, "UserAccounts.Userlevel")));
3911 free(rows->rows);
3912 free(rows->fieldNames);
3913 free(rows);
3914 }
3915 else
3916 {
3917 Rd->database->putstr(Rd->database, "UserAccounts.PrincipalID", xstrdup(uuid));
3918 }
3919 }
3920
3921 return ret;
3922}
3923
3924
3925
3926int validateThings(reqData *Rd, char *doit, char *name, qhashtbl_t *things)
3927{
3928 int e = 0;
3929
3930 W("%s start of %s validation.", doit, name);
3931 qlisttbl_obj_t obj;
3932 memset((void *) &obj, 0, sizeof(obj));
3933 fieldValidFuncs->lock(fieldValidFuncs);
3934 while(fieldValidFuncs->getnext(fieldValidFuncs, &obj, NULL, false) == true)
3935 {
3936 char *t = getStrH(things, obj.name);
3937
3938 if ('\0' != t[0])
3939 {
3940 validFunc *vf = (validFunc *) obj.data;
3941
3942W("Validating %s", obj.name);
3943 if (vf->func)
3944 e += vf->func(Rd, things);
3945 else
3946 E("No validation function for %s", obj.name);
3947 }
3948 }
3949 fieldValidFuncs->unlock(fieldValidFuncs);
3950 return e;
3951}
3952
3953
3954void loginPage(reqData *Rd, char *message)
3955{
3956 char *name = xstrdup(getStrH(Rd->stuff, "name"));
3957
3958 Rd->stuff->remove(Rd->stuff, "UUID");
3959 HTMLheader(Rd->reply, "<!--#echo var=\"grid\" --> account manager");
3960 HTMLdebug(Rd->reply);
3961 Rd->reply->addstrf(Rd->reply, "<h1><!--#echo var=\"grid\" --> account manager</h1>\n");
3962 Rd->reply->addstr(Rd->reply, checkLinky(Rd));
3963 if (0 != Rd->errors->size(Rd->messages))
3964 HTMLlist(Rd->reply, "messages -", Rd->messages);
3965 HTMLform(Rd->reply, "", Rd->shs.munchie);
3966 HTMLtext(Rd->reply, "text", "name", "name", name, 42, 63, TRUE);
3967 HTMLtext(Rd->reply, "password", "password", "password", "", 16, 0, TRUE);
3968 Rd->reply->addstr(Rd->reply, "<p>Warning, the limit on password length is set by your viewer, some can't handle longer than 16 characters.</p>\n");
3969 Rd->reply->addstr(Rd->reply, "<p>While viewers will usually remember your name and password for you, you'll need to remember it for this web site to. %nbsp; "
3970 "I highly recommend using a password manager. KeePass and it's variations is a great password manager.</p>\n");
3971 Rd->reply->addstrf(Rd->reply, "<input type='submit' disabled style='display: none' aria-hidden='true' />"); // Stop Enter key on text fields triggering the first submit button.
3972 HTMLbutton(Rd->reply, "login");
3973 HTMLbutton(Rd->reply, "create");
3974 if (0 != Rd->errors->size(Rd->errors))
3975 HTMLlist(Rd->reply, "errors -", Rd->errors);
3976 Rd->reply->addstrf(Rd->reply, "<p>%s</p>\n", message);
3977 HTMLfooter(Rd->reply);
3978 free(name);
3979}
3980
3981void accountCreationPage(reqData *Rd, char *message)
3982{
3983 char *name = getStrH(Rd->stuff, "name");
3984 char *toke_n_munchie = getCookie(Rd->Rcookies, "toke_n_munchie");
3985 char *tmp = xmalloc(16), *t;
3986 int i, d;
3987
3988// TODO - eww lots of memory leaks here.
3989// TODO - need to check if qlibc does it's own free() calls, and fill in the gaps for when it doesn't.
3990 HTMLheader(Rd->reply, "<!--#echo var=\"grid\" --> account manager");
3991 HTMLdebug(Rd->reply);
3992 Rd->reply->addstrf(Rd->reply, "<h1><!--#echo var=\"grid\" --> account manager</h1>\n");
3993 Rd->reply->addstrf(Rd->reply, "<h2>Creating <!--#echo var=\"grid\" --> account for %s</h2>\n", name);
3994 Rd->reply->addstr(Rd->reply, checkLinky(Rd));
3995 if (0 != Rd->errors->size(Rd->messages))
3996 HTMLlist(Rd->reply, "messages -", Rd->messages);
3997 HTMLform(Rd->reply, "", Rd->shs.munchie);
3998 HTMLhidden(Rd->reply, "name", name);
3999 HTMLhidden(Rd->reply, "UUID", getStrH(Rd->stuff, "UUID"));
4000 HTMLhidden(Rd->reply, "psswrd", getStrH(Rd->body, "password"));
4001 HTMLtext(Rd->reply, "email", "email", "email", getStrH(Rd->stuff, "email"), 42, 254, FALSE);
4002 HTMLtext(Rd->reply, "email", "Repeat your email, to be sure you got it correct", "emayl", getStrH(Rd->stuff, "emayl"), 42, 254, FALSE);
4003 Rd->reply->addstr(Rd->reply, "<p>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.</p>\n");
4004 HTMLtext(Rd->reply, "password", "Re-enter your password", "password", "", 16, 0, FALSE);
4005 Rd->reply->addstr(Rd->reply, "<p>Warning, the limit on password length is set by your viewer, some can't handle longer than 16 characters.</p>\n");
4006 Rd->reply->addstr(Rd->reply, "<p>While viewers will usually remember your name and password for you, you'll need to remember it for this web site to. %nbsp; "
4007 "I highly recommend using a password manager. KeePass and it's variations is a great password manager.</p>\n");
4008 Rd->reply->addstr(Rd->reply, "<table><tr><td>");
4009 HTMLselect(Rd->reply, "Date of birth", "year");
4010 t = getStrH(Rd->stuff, "year");
4011 if (NULL == t)
4012 d = -1;
4013 else
4014 d = atoi(t);
4015 HTMLoption(Rd->reply, xstrdup(""), FALSE);
4016 for (i = 1900; i <= 2020; i++)
4017 {
4018 boolean sel = FALSE;
4019
4020 if (i == d)
4021 sel = TRUE;
4022 sprintf(tmp, "%d", i);
4023 HTMLoption(Rd->reply, xstrdup(tmp), sel);
4024 }
4025 HTMLselectEnd(Rd->reply);
4026 Rd->reply->addstr(Rd->reply, "</tt><td>");
4027 HTMLselect(Rd->reply, NULL, "month");
4028 t = getStrH(Rd->stuff, "month");
4029 HTMLoption(Rd->reply, xstrdup(""), FALSE);
4030 for (i = 0; i <= 11; i++)
4031 {
4032 boolean sel = FALSE;
4033
4034 if ((NULL != t) && (strcmp(t, months[i]) == 0))
4035 sel = TRUE;
4036 HTMLoption(Rd->reply, months[i], sel);
4037 }
4038 HTMLselectEnd(Rd->reply);
4039 Rd->reply->addstr(Rd->reply, "</td></tr></table>");
4040 HTMLcheckBox(Rd->reply, "adult", "I'm allegedly an adult in my country.", !strcmp("on", getStrH(Rd->stuff, "adult")));
4041 HTMLcheckBox(Rd->reply, "agree", "I accept the Terms & Conditions of Use.", !strcmp("on", getStrH(Rd->stuff, "agree")));
4042 Rd->reply->addstrf(Rd->reply, "<input type='submit' disabled style='display: none' aria-hidden='true' />"); // Stop Enter key on text fields triggering the first submit button.
4043 HTMLbutton(Rd->reply, "confirm");
4044 HTMLbutton(Rd->reply, "cancel");
4045 if (0 != Rd->errors->size(Rd->errors))
4046 HTMLlist(Rd->reply, "errors -", Rd->errors);
4047 Rd->reply->addstrf(Rd->reply, "<p>%s</p>\n", message);
4048 HTMLfooter(Rd->reply);
4049}
4050
4051void loggedOnPage(reqData *Rd, char *message)
4052{
4053 char *name = getStrH(Rd->stuff, "name");
4054 char *toke_n_munchie = getCookie(Rd->Rcookies, "toke_n_munchie");
4055
4056 HTMLheader(Rd->reply, "<!--#echo var=\"grid\" --> account manager");
4057 HTMLdebug(Rd->reply);
4058 Rd->reply->addstrf(Rd->reply, "<h1><!--#echo var=\"grid\" --> account manager</h1>\n");
4059 Rd->reply->addstrf(Rd->reply, "<h2><!--#echo var=\"grid\" --> account for %s</h2>\n", name);
4060 Rd->reply->addstr(Rd->reply, checkLinky(Rd));
4061 if (0 != Rd->errors->size(Rd->messages))
4062 HTMLlist(Rd->reply, "messages -", Rd->messages);
4063 HTMLform(Rd->reply, "", Rd->shs.munchie);
4064 HTMLhidden(Rd->reply, "name", name);
4065 HTMLhidden(Rd->reply, "UUID", getStrH(Rd->stuff, "UUID"));
4066 HTMLtext(Rd->reply, "email", "email", "email", getStrH(Rd->database, "UserAccounts.Email"), 42, 254, FALSE);
4067 HTMLtext(Rd->reply, "Old password", "password", "password", "", 16, 0, FALSE);
4068 Rd->reply->addstr(Rd->reply, "<p>Warning, the limit on password length is set by your viewer, some can't handle longer than 16 characters.</p>\n");
4069// HTMLtext(Rd->reply, "title", "text", "title", getStrH(Rh->stuff, "title"), 16, 64, TRUE);
4070 HTMLselect(Rd->reply, "type", "type");
4071 HTMLoption(Rd->reply, "", false);
4072 HTMLoption(Rd->reply, "newbie", true);
4073 HTMLoption(Rd->reply, "validated", true);
4074 HTMLoption(Rd->reply, "vouched for", true);
4075 HTMLoption(Rd->reply, "approved", true);
4076 HTMLoption(Rd->reply, "disabled", false);
4077 HTMLoption(Rd->reply, "god", false);
4078 HTMLselectEnd(Rd->reply);
4079 Rd->reply->addstrf(Rd->reply, "<input type='submit' disabled style='display: none' aria-hidden='true' />"); // Stop Enter key on text fields triggering the first submit button.
4080 HTMLbutton(Rd->reply, "delete");
4081 HTMLbutton(Rd->reply, "list");
4082 HTMLbutton(Rd->reply, "logout");
4083 HTMLbutton(Rd->reply, "update");
4084 if (0 != Rd->errors->size(Rd->errors))
4085 HTMLlist(Rd->reply, "errors -", Rd->errors);
4086 HTMLfooter(Rd->reply);
4087}
4088
4089void listPage(reqData *Rd, char *message)
4090{
4091// TODO - should check if the user is a god before allowing this.
4092 char *name = getStrH(Rd->stuff, "name");
4093 char *toke_n_munchie = getCookie(Rd->Rcookies, "toke_n_munchie");
4094
4095 HTMLheader(Rd->reply, "<!--#echo var=\"grid\" --> account manager");
4096 HTMLdebug(Rd->reply);
4097 Rd->reply->addstrf(Rd->reply, "<h1><!--#echo var=\"grid\" --> account manager</h1>\n");
4098 Rd->reply->addstrf(Rd->reply, "<h1><!--#echo var=\"grid\" --> member accounts</h1>\n");
4099 Rd->reply->addstr(Rd->reply, checkLinky(Rd));
4100 if (0 != Rd->errors->size(Rd->messages))
4101 HTMLlist(Rd->reply, "messages -", Rd->messages);
4102 HTMLtable(Rd->reply, Rd->db,
4103 dbSelect(Rd->db, "UserAccounts",
4104 "CONCAT(FirstName,' ',LastName) as Name,UserTitle as Title,UserLevel as Level,UserFlags as Flags,PrincipalID as UUID",
4105 NULL, NULL, "Name"),
4106 "member accounts", NULL, NULL);
4107 HTMLform(Rd->reply, "", Rd->shs.munchie);
4108 HTMLhidden(Rd->reply, "name", name);
4109 HTMLhidden(Rd->reply, "UUID", getStrH(Rd->stuff, "UUID"));
4110 Rd->reply->addstrf(Rd->reply, "<input type='submit' disabled style='display: none' aria-hidden='true' />"); // Stop Enter key on text fields triggering the first submit button.
4111 HTMLbutton(Rd->reply, "me");
4112 HTMLbutton(Rd->reply, "logout");
4113 if (0 != Rd->errors->size(Rd->errors))
4114 HTMLlist(Rd->reply, "errors -", Rd->errors);
4115 Rd->reply->addstrf(Rd->reply, "<p>%s</p>\n", message);
4116 HTMLfooter(Rd->reply);
4117}
4118
4119void account_html(char *file, reqData *Rd, HTMLfile *thisFile)
4120{
4121 boolean isGET = FALSE;
4122 int e = 0;
4123 char *doit = getStrH(Rd->body, "doit");
4124
4125 C("Starting dynamic page %s", file);
4126 Rd->func = NULL;
4127
4128 if (NULL == fieldValidFuncs)
4129 {
4130 fieldValidFuncs = qlisttbl(QLISTTBL_LOOKUPFORWARD | QLISTTBL_THREADSAFE | QLISTTBL_UNIQUE);
4131 newValidFunc("hashish", (fieldValidFunc) validateSesh);
4132 newValidFunc("toke_n_munchie", (fieldValidFunc) validateSesh);
4133 newValidFunc("UUID", (fieldValidFunc) validateUUID);
4134 newValidFunc("name", (fieldValidFunc) validateName);
4135 newValidFunc("password", (fieldValidFunc) validatePassword);
4136 newValidFunc("psswrd", (fieldValidFunc) validatePassword);
4137 newValidFunc("email", (fieldValidFunc) validateEmail);
4138 newValidFunc("emayl", (fieldValidFunc) validateEmail);
4139 newValidFunc("year", (fieldValidFunc) validateDoB);
4140 newValidFunc("month", (fieldValidFunc) validateDoB);
4141 newValidFunc("adult", (fieldValidFunc) validateLegal);
4142 newValidFunc("agree", (fieldValidFunc) validateLegal);
4143 }
4144 if (NULL == buildPages)
4145 {
4146 buildPages = qhashtbl(0, 0);
4147 newBuildPage("login", (pageBuildFunction) loggedOnPage, (pageBuildFunction) loginPage);
4148 newBuildPage("cancel", (pageBuildFunction) loginPage, (pageBuildFunction) loginPage);
4149 newBuildPage("logout", (pageBuildFunction) loginPage, (pageBuildFunction) loginPage);
4150 newBuildPage("create", (pageBuildFunction) accountCreationPage, (pageBuildFunction) loginPage);
4151 newBuildPage("confirm", (pageBuildFunction) loggedOnPage, (pageBuildFunction) accountCreationPage);
4152 newBuildPage("me", (pageBuildFunction) loggedOnPage, (pageBuildFunction) loginPage);
4153 newBuildPage("update", (pageBuildFunction) loggedOnPage, (pageBuildFunction) loginPage);
4154 newBuildPage("delete", (pageBuildFunction) loginPage, (pageBuildFunction) loginPage);
4155 newBuildPage("list", (pageBuildFunction) listPage, (pageBuildFunction) loginPage);
4156 }
4157
4158 if ('\0' == doit[0])
4159 doit = getStrH(Rd->cookies, "doit");
4160 if ('\0' == doit[0])
4161 doit = "logout";
4162 if ('\0' != doit[0])
4163 {
4164 setCookie(Rd, "doit", doit);
4165 Rd->doit = doit;
4166 }
4167
4168 e += validateThings(Rd, doit, "cookies", Rd->cookies);
4169 e += validateThings(Rd, doit, "body", Rd->body);
4170 e += validateThings(Rd, doit, "queries", Rd->queries);
4171 e += validateThings(Rd, doit, "session", Rd->stuff);
4172
4173 if (NULL == Rd->func)
4174 {
4175 buildPage *bp = buildPages->get(buildPages, doit, NULL, false);
4176 if (bp)
4177 {
4178 if (e)
4179 {
4180 Rd->func = bp->eFunc;
4181 E("Got page builder ERROR function for %s, coz of %d errors.", doit, e);
4182 }
4183 else
4184 {
4185 Rd->func = bp->func;
4186 D("Got page builder function for %s.", doit);
4187 }
4188 }
4189 }
4190 if (NULL == Rd->func)
4191 Rd->func = (pageBuildFunction) loginPage;
4192
4193 if (strcmp("https", Rd->Scheme) != 0)
4194 {
4195 Rd->Rheaders->putstr (Rd->Rheaders, "Status", "301 Moved Permanently");
4196 Rd->Rheaders->putstrf(Rd->Rheaders, "Location", "https://%s%s", Rd->Host, Rd->RUri);
4197 Rd->reply->addstrf(Rd->reply, "<html><title>404 Unknown page</title><head>"
4198 "<meta http-equiv='refresh' content='0; URL=https://%s%s' />"
4199 "</head><body>You should get redirected to <a href='https://%s%s'>https://%s%s</a></body></html>",
4200 Rd->Host, Rd->RUri, Rd->Host, Rd->RUri, Rd->Host, Rd->RUri
4201 );
4202 D("Redirecting dynamic page %s -> https://%s%s", file, Rd->Host, Rd->RUri);
4203 return;
4204 }
4205
4206 // Check "Origin" header and /or HTTP_REFERER header.
4207 // "Origin" is either HTTP_HOST or X-FORWARDED-HOST. Which could be "null".
4208 char *ref = xmprintf("https://%s%s/account.html", getStrH(Rd->headers, "SERVER_NAME"), getStrH(Rd->headers, "SCRIPT_NAME"));
4209 char *href = Rd->headers->getstr(Rd->headers, "HTTP_REFERER", true);
4210
4211 if (NULL != href)
4212 {
4213 char *f = strchr(href, '?');
4214
4215 if (NULL != f)
4216 *f = '\0';
4217 if (('\0' != href[0]) && (strcmp(ref, href) != 0))
4218 {
4219 bitch(Rd, "Invalid referer.", ref);
4220 D("Invalid referer - %s isn't %s", ref, href);
4221 Rd->func = (pageBuildFunction) loginPage;
4222 }
4223 free(href);
4224 }
4225 free(ref);
4226 ref = getStrH(Rd->headers, "SERVER_NAME");
4227 href = getStrH(Rd->headers, "HTTP_HOST");
4228 if ('\0' == href[0])
4229 href = getStrH(Rd->headers, "X-FORWARDED-HOST");
4230 if (('\0' != href[0]) && (strcmp(ref, href) != 0))
4231 {
4232 bitch(Rd, "Invalid HOST.", ref);
4233 D("Invalid HOST - %s isn't %s", ref, href);
4234 Rd->func = (pageBuildFunction) loginPage;
4235 }
4236
4237 // Redirect to a GET if it was a POST.
4238 if ((0 == e) && (strcmp("POST", Rd->Method) == 0))
4239 {
4240 if (Rd->func == (pageBuildFunction) loginPage)
4241 freeSesh(Rd, FALSE, TRUE);
4242
4243 if (strcmp("confirm", doit) == 0)
4244 {
4245 createUser(Rd);
4246 newSesh(Rd, TRUE);
4247 Rd->chillOut = TRUE;
4248 }
4249
4250 if (strcmp("login", doit) == 0)
4251 Rd->chillOut = TRUE;
4252
4253 if (Rd->vegOut)
4254 {
4255 T("vegOut");
4256 freeSesh(Rd, FALSE, TRUE);
4257 }
4258 else if (Rd->chillOut)
4259 {
4260 T("chillOut");
4261 freeSesh(Rd, FALSE, FALSE);
4262 newSesh(Rd, FALSE);
4263 }
4264 else if ('\0' == Rd->shs.leaf[0])
4265 newSesh(Rd, FALSE);
4266
4267 Rd->Rheaders->putstr (Rd->Rheaders, "Status", "303 See Other");
4268 Rd->Rheaders->putstrf(Rd->Rheaders, "Location", "https://%s%s", Rd->Host, Rd->RUri);
4269 Rd->reply->addstrf(Rd->reply, "<html><title>Post POST redirect</title><head>"
4270 "<meta http-equiv='refresh' content='0; URL=https://%s%s' />"
4271 "</head><body>You should get redirected to <a href='https://%s%s'>https://%s%s</a></body></html>",
4272 Rd->Host, Rd->RUri, Rd->Host, Rd->RUri, Rd->Host, Rd->RUri
4273 );
4274 I("Redirecting dynamic page %s -> https://%s%s", file, Rd->Host, Rd->RUri);
4275 }
4276 else // Actually send the page.
4277 {
4278 if (Rd->func == (pageBuildFunction) loginPage)
4279 {
4280 if (strcmp("confirm", doit) != 0)
4281 freeSesh(Rd, FALSE, TRUE);
4282 newSesh(Rd, FALSE);
4283 }
4284 Rd->func(Rd, "");
4285 }
4286
4287 C("Ending dynamic page %s", file);
4288}
4289
4290
4291void sledjchisl_main(void)
4292//int main(int argc, char *argv[], char **env)
4293{
4294 // Don't segfault if our environment is crazy.
4295// if (!*argv) return 127;
4296
4297 char *cmd = *toys.optargs;
4298
4299
4300// char *cmd = *argv;
4301 char *tmp;
4302 qhashtbl_t *configs = qhashtbl(0, 0);
4303 lua_State *L = luaL_newstate();
4304 MYSQL *database = NULL, *dbconn = NULL;
4305 gridStats *stats = NULL;
4306 struct stat statbuf;
4307 int status, result, i;
4308 void *vd;
4309
4310 pwd = getcwd(0, 0);
4311
4312 if (-1 == fstat(STDIN_FILENO, &statbuf))
4313 {
4314 error_msg("fstat() failed");
4315 if (1 != isatty(STDIN_FILENO))
4316 isWeb = TRUE;
4317 }
4318 else
4319 {
4320 if (S_ISREG (statbuf.st_mode)) D("regular file");
4321 else if (S_ISDIR (statbuf.st_mode)) D("directory");
4322 else if (S_ISCHR (statbuf.st_mode)) D("character device");
4323 else if (S_ISBLK (statbuf.st_mode)) D("block device");
4324 else if (S_ISFIFO(statbuf.st_mode)) D("FIFO (named pipe)");
4325 else if (S_ISLNK (statbuf.st_mode)) D("symbolic link");
4326 else if (S_ISSOCK(statbuf.st_mode)) D("socket");
4327 else D("unknown file descriptor type");
4328 if (!S_ISCHR(statbuf.st_mode))
4329 isWeb = TRUE;
4330 }
4331 if (!isWeb)
4332 {
4333 I("Outputting to a terminal, not a web server.");
4334 // Check if we are already running inside the proper tmux server.
4335 char *eTMUX = getenv("TMUX");
4336 memset(toybuf, 0, sizeof(toybuf));
4337 snprintf(toybuf, sizeof(toybuf), "%s/%s", scRoot, Tsocket);
4338 if (((eTMUX) && (0 == strncmp(toybuf, eTMUX, strlen(toybuf)))))
4339 {
4340 I("Running inside the proper tmux server.");
4341 isTmux = TRUE;
4342 }
4343 else
4344 I("Not running inside the proper tmux server, starting it.");
4345 I("libfcgi version: %s", FCGI_VERSION);
4346 I("Lua version: %s", LUA_RELEASE);
4347 I("LuaJIT version: %s", LUAJIT_VERSION);
4348 I("MariaDB / MySQL client version: %s", mysql_get_client_info());
4349 }
4350
4351 // Print our user name and groups.
4352 struct passwd *pw;
4353 struct group *grp;
4354 uid_t euid = geteuid();
4355 gid_t egid = getegid();
4356 gid_t *groups = (gid_t *)toybuf;
4357 i = sizeof(toybuf)/sizeof(gid_t);
4358 int ngroups;
4359
4360 pw = xgetpwuid(euid);
4361 D("Running as user %s", pw->pw_name);
4362
4363 grp = xgetgrgid(egid);
4364 ngroups = getgroups(i, groups);
4365 if (ngroups < 0) perror_exit("getgroups");
4366 D("User is in group %s", grp->gr_name);
4367 for (i = 0; i < ngroups; i++) {
4368 if (groups[i] != egid)
4369 {
4370 if ((grp = getgrgid(groups[i])))
4371 D("User is in group %s", grp->gr_name);
4372 else
4373 D("User is in group %u", groups[i]);
4374 }
4375 }
4376
4377
4378/* From http://luajit.org/install.html -
4379To change or extend the list of standard libraries to load, copy
4380src/lib_init.c to your project and modify it accordingly. Make sure the
4381jit library is loaded or the JIT compiler will not be activated.
4382*/
4383 luaL_openlibs(L); // Load Lua libraries.
4384
4385 // Load the config scripts.
4386 char *cPaths[] =
4387 {
4388 "/etc/sledjChisl.conf.lua",
4389// "/etc/sledjChisl.d/*.lua",
4390 "~/.sledjChisl.conf.lua",
4391// "~/.config/sledjChisl/*.lua",
4392 ".sledjChisl.conf.lua",
4393 NULL
4394 };
4395 struct stat st;
4396
4397
4398 for (i = 0; cPaths[i]; i++)
4399 {
4400 memset(toybuf, 0, sizeof(toybuf));
4401 if (('/' == cPaths[i][0]) || ('~' == cPaths[i][0]))
4402 snprintf(toybuf, sizeof(toybuf), "%s", cPaths[i]);
4403 else
4404 snprintf(toybuf, sizeof(toybuf), "%s/%s", pwd, cPaths[i]);
4405 if (0 != lstat(toybuf, &st))
4406 continue;
4407 if (!isWeb) I("Loading configuration file - %s", toybuf);
4408 status = luaL_loadfile(L, toybuf);
4409 if (status) // If something went wrong, error message is at the top of the stack.
4410 E("Couldn't load file: %s", lua_tostring(L, -1));
4411 else
4412 {
4413 result = lua_pcall(L, 0, LUA_MULTRET, 0);
4414 if (result)
4415 E("Failed to run script: %s", lua_tostring(L, -1));
4416 else
4417 {
4418 lua_getglobal(L, "config");
4419 lua_pushnil(L);
4420 while(lua_next(L, -2) != 0)
4421 {
4422 char *n = (char *) lua_tostring(L, -2);
4423
4424 // Numbers can convert to strings, so check for numbers before checking for strings.
4425 // On the other hand, strings that can be converted to numbers also pass lua_isnumber(). sigh
4426 if (lua_isnumber(L, -1))
4427 {
4428 float v = lua_tonumber(L, -1);
4429 configs->put(configs, n, &v, sizeof(float));
4430 }
4431 else if (lua_isstring(L, -1))
4432 configs->putstr(configs, n, (char *) lua_tostring(L, -1));
4433 else
4434 {
4435 char *v = (char *) lua_tostring(L, -1);
4436 E("Unknown config variable type for %s = %s", n, v);
4437 }
4438 lua_pop(L, 1);
4439 }
4440 }
4441 }
4442 }
4443 if ((vd = configs->get (configs, "loadAverageInc", NULL, false)) != NULL) {loadAverageInc = *((float *) vd); D("Setting loadAverageInc = %f", loadAverageInc);}
4444 if ((vd = configs->get (configs, "simTimeOut", NULL, false)) != NULL) {simTimeOut = (int) *((float *) vd); D("Setting simTimeOut = %d", simTimeOut);}
4445 if ((tmp = configs->getstr(configs, "scRoot", false)) != NULL) {scRoot = tmp; D("Setting scRoot = %s", scRoot);}
4446 if ((tmp = configs->getstr(configs, "scUser", false)) != NULL) {scUser = tmp; D("Setting scUser = %s", scUser);}
4447 if ((tmp = configs->getstr(configs, "Tconsole", false)) != NULL) {Tconsole = tmp; D("Setting Tconsole = %s", Tconsole);}
4448 if ((tmp = configs->getstr(configs, "Tsocket", false)) != NULL) {Tsocket = tmp; D("Setting Tsocket = %s", Tsocket);}
4449 if ((tmp = configs->getstr(configs, "Ttab", false)) != NULL) {Ttab = tmp; D("Setting Ttab = %s", Ttab);}
4450 if ((tmp = configs->getstr(configs, "webRoot", false)) != NULL) {webRoot = tmp; D("Setting webRoot = %s", webRoot);}
4451 if ((tmp = configs->getstr(configs, "URL", false)) != NULL) {URL = tmp; D("Setting URL = %s", URL);}
4452 if ((vd = configs->get (configs, "seshTimeOut", NULL, false)) != NULL) {seshTimeOut = (int) *((float *) vd); D("Setting seshTimeOut = %d", seshTimeOut);}
4453 if ((vd = configs->get (configs, "idleTimeOut", NULL, false)) != NULL) {idleTimeOut = (int) *((float *) vd); D("Setting idleTimeOut = %d", idleTimeOut);}
4454 if ((vd = configs->get (configs, "newbieTimeOut", NULL, false)) != NULL) {newbieTimeOut = (int) *((float *) vd); D("Setting newbieTimeOut = %d", newbieTimeOut);}
4455
4456
4457 if (isTmux || isWeb)
4458 {
4459 char *d;
4460
4461 mimeTypes = qhashtbl(0, 0);
4462 mimeTypes->putstr(mimeTypes, "gz", "application/gzip");
4463 mimeTypes->putstr(mimeTypes, "js", "application/javascript");
4464 mimeTypes->putstr(mimeTypes, "json", "application/json");
4465 mimeTypes->putstr(mimeTypes, "pdf", "application/pdf");
4466 mimeTypes->putstr(mimeTypes, "rtf", "application/rtf");
4467 mimeTypes->putstr(mimeTypes, "zip", "application/zip");
4468 mimeTypes->putstr(mimeTypes, "xz", "application/x-xz");
4469 mimeTypes->putstr(mimeTypes, "gif", "image/gif");
4470 mimeTypes->putstr(mimeTypes, "png", "image/png");
4471 mimeTypes->putstr(mimeTypes, "jp2", "image/jp2");
4472 mimeTypes->putstr(mimeTypes, "jpg2", "image/jp2");
4473 mimeTypes->putstr(mimeTypes, "jpe", "image/jpeg");
4474 mimeTypes->putstr(mimeTypes, "jpg", "image/jpeg");
4475 mimeTypes->putstr(mimeTypes, "jpeg", "image/jpeg");
4476 mimeTypes->putstr(mimeTypes, "svg", "image/svg+xml");
4477 mimeTypes->putstr(mimeTypes, "svgz", "image/svg+xml");
4478 mimeTypes->putstr(mimeTypes, "tif", "image/tiff");
4479 mimeTypes->putstr(mimeTypes, "tiff", "image/tiff");
4480 mimeTypes->putstr(mimeTypes, "css", "text/css");
4481 mimeTypes->putstr(mimeTypes, "html", "text/html");
4482 mimeTypes->putstr(mimeTypes, "htm", "text/html");
4483 mimeTypes->putstr(mimeTypes, "shtml", "text/html");
4484// mimeTypes->putstr(mimeTypes, "md", "text/markdown");
4485// mimeTypes->putstr(mimeTypes, "markdown", "text/markdown");
4486 mimeTypes->putstr(mimeTypes, "txt", "text/plain");
4487
4488 memset(toybuf, 0, sizeof(toybuf));
4489 snprintf(toybuf, sizeof(toybuf), "%s/config/config.ini", scRoot);
4490
4491// TODO - it looks like OpenSim invented their own half arsed backwards INI file include system.
4492// I doubt qlibc supports it, like it supports what seems to be the standard include system.
4493// Not sure if we need to worry about it just yet.
4494 qlisttbl_t *qconfig = qconfig_parse_file(NULL, toybuf, '=');
4495 if (NULL == qconfig)
4496 {
4497 E("Can't read config file %s", toybuf);
4498 goto finished;
4499 }
4500 d = qstrunchar(qconfig->getstr(qconfig, "Const.ConnectionString", false), '"', '"');
4501
4502 if (NULL == d)
4503 {
4504 E("No database credentials in %s!", toybuf);
4505 goto finished;
4506 }
4507 else
4508 {
4509 char *p0, *p1, *p2;
4510 if (NULL == (d = strdup(d)))
4511 {
4512 E("Out of memory!");
4513 goto finished;
4514 }
4515 // Data Source=MYSQL_HOST;Database=MYSQL_DB;User ID=MYSQL_USER;Password=MYSQL_PASSWORD;Old Guids=true;
4516 p0 = d;
4517 while (NULL != p0)
4518 {
4519 p1 = strchr(p0, '=');
4520 if (NULL == p1) break;
4521 *p1 = '\0';
4522 p2 = strchr(p1 + 1, ';');
4523 if (NULL == p2) break;
4524 *p2 = '\0';
4525 configs->putstr(configs, p0, p1 + 1); // NOTE - this allocs memory for it's key and it's data.
4526 p0 = p2 + 1;
4527 if ('\0' == *p0)
4528 p0 = NULL;
4529 };
4530 free(d);
4531 }
4532
4533// TODO - should also load god names, and maybe the SMTP stuff.
4534// Note that the OpenSim SMTP stuff is ONLY for LSL script usage, we probably want to put it in the Lua config file instead.
4535
4536 if (mysql_library_init(toys.optc, toys.optargs, NULL))
4537// if (mysql_library_init(argc, argv, NULL))
4538 {
4539 E("mysql_library_init() failed!");
4540 goto finished;
4541 }
4542 database = mysql_init(NULL);
4543 if (NULL == database)
4544 {
4545 E("mysql_init() failed - %s", mysql_error(database));
4546 goto finished;
4547 }
4548 else
4549 {
4550 dbconn = mysql_real_connect(database,
4551 configs->getstr(configs, "Data Source", true),
4552 configs->getstr(configs, "User ID", true),
4553 configs->getstr(configs, "Password", true),
4554 configs->getstr(configs, "Database", true),
4555// 3036, "/var/run/mysqld/mysqld.sock",
4556 0, NULL,
4557 CLIENT_FOUND_ROWS | CLIENT_LOCAL_FILES | CLIENT_MULTI_STATEMENTS | CLIENT_MULTI_RESULTS);
4558 if (NULL == dbconn)
4559 {
4560 E("mysql_real_connect() failed - %s", mysql_error(database));
4561 goto finished;
4562 }
4563
4564 // Need to kick this off.
4565 stats = getStats(database, stats);
4566 char *h = qstrunchar(qconfig->getstr(qconfig, "Const.HostName", false), '"', '"');
4567 char *p = qstrunchar(qconfig->getstr(qconfig, "Const.PublicPort", false), '"', '"');
4568 stats->stats->putstr(stats->stats, "grid", qstrunchar(qconfig->getstr(qconfig, "Const.GridName", false), '"', '"'));
4569 stats->stats->putstr(stats->stats, "HostName", h);
4570 stats->stats->putstr(stats->stats, "PublicPort", p);
4571 snprintf(toybuf, sizeof(toybuf), "http://%s:%s/", h, p);
4572
4573 stats->stats->putstr(stats->stats, "uri", toybuf);
4574 }
4575 }
4576
4577
4578 if (isWeb)
4579 {
4580 char **initialEnv = environ;
4581 char *tmp0, *tmp1;
4582 int count = 0, entries, bytes;
4583
4584 dynPages = qhashtbl(0, 0);
4585 newDynPage("account.html", (pageFunction) account_html);
4586
4587 // FCGI_LISTENSOCK_FILENO is the socket to the web server.
4588 // STDOUT and STDERR go to the web servers error log, or at least it does in Apache 2 mod_fcgid.
4589 I("Running SledjChisl inside a web server, pid %d.", getpid());
4590
4591 if (0 == toys.optc)
4592// if (1 == argc)
4593 D("no args");
4594 else
4595 {
4596 for (entries = 0, bytes = -1; entries < toys.optc; entries++)
4597 D("ARG %s\n", toys.optargs[entries]);
4598// for (i = 0; argv[i] != NULL; i++)
4599// D("ARG %s", argv[i]);
4600 }
4601// printEnv(env);
4602 printEnv(environ);
4603
4604/*
4605? https://stackoverflow.com/questions/30707792/how-to-disable-buffering-with-apache2-and-mod-proxy-fcgi
4606 https://z-issue.com/wp/apache-2-4-the-event-mpm-php-via-mod_proxy_fcgi-and-php-fpm-with-vhosts/
4607 A lengthy and detailed "how to set this up with PHP" that might be useful.
4608 https://www.apachelounge.com/viewtopic.php?t=4385
4609 "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."
4610 But then so is the silly fcgi2 SDK, which basically assumes it's a CGI wrapper, not proper FCGI.
4611+ I could even start the spawn-fcgi process from the tmux instance of sledjchisl.
4612+ Orrr just open the socket / port myself from the tmux instance and do the raw FCGI thing through it.
4613*/
4614 while (FCGI_Accept() != -1)
4615 {
4616 reqData *Rd = xzalloc(sizeof(reqData));
4617 if (-1 == clock_gettime(CLOCK_REALTIME, &Rd->then))
4618 perror_msg("Unable to get the time.");
4619 Rd->L = L;
4620 Rd->configs = configs;
4621// Rd->queries = qhashtbl(0, 0); // Inited in toknize below.
4622// Rd->body = qhashtbl(0, 0); // Inited in toknize below.
4623// Rd->cookies = qhashtbl(0, 0); // Inited in toknize below.
4624 Rd->headers = qhashtbl(0, 0);
4625 Rd->stuff = qhashtbl(0, 0);
4626 Rd->database = qhashtbl(0, 0);
4627 Rd->Rcookies = qhashtbl(0, 0);
4628 Rd->Rheaders = qhashtbl(0, 0);
4629 Rd->db = database;
4630 Rd->stats = stats;
4631 Rd->errors = qlist(0);
4632 Rd->messages = qlist(0);
4633 Rd->reply = qgrow(QGROW_THREADSAFE);
4634 qhashtbl_obj_t hobj;
4635 qlist_obj_t lobj;
4636
4637 // So far I'm seeing these as all caps, but I don't think they are defined that way. Case insensitive per the spec.
4638 // So convert them now, also "-" -> "_".
4639T("HEADERS");
4640 char **envp = environ;
4641 for ( ; *envp != NULL; envp++)
4642 {
4643 char *k = xstrdup(*envp);
4644 char *v = strchr(k, '=');
4645 if (NULL != v)
4646 {
4647 *v = '\0';
4648 char *ky = qstrreplace("tr", qstrupper(k), "-", "_");
4649 Rd->headers->putstr(Rd->headers, ky, v + 1);
4650if ((strcmp("HTTP_COOKIE", ky) == 0) || (strcmp("CONTENT_LENGTH", ky) == 0) || (strcmp("QUERY_STRING", ky) == 0))
4651 D(" %s = %s", ky, v + 1);
4652 }
4653 }
4654
4655 // The FCGI paramaters sent from the server, are converted to environment variablse for the fcgi2 SDK.
4656 // The FCGI spec doesn't mention what these are. except FCGI_WEB_SERVER_ADDRS.
4657 char *Role = Rd->headers->getstr(Rd->headers, "FCGI_ROLE", false);
4658 char *Path = Rd->headers->getstr(Rd->headers, "PATH_INFO", false);
4659// if (NULL == Path) {msleep(1000); continue;}
4660 char *Length = Rd->headers->getstr(Rd->headers, "CONTENT_LENGTH", false);
4661//char *Type = Rd->headers->getstr(Rd->headers, "CONTENT_TYPE", false);
4662 Rd->Method = Rd->headers->getstr(Rd->headers, "REQUEST_METHOD", false);
4663 Rd->Script = Rd->headers->getstr(Rd->headers, "SCRIPT_NAME", false);
4664 Rd->Scheme = Rd->headers->getstr(Rd->headers, "REQUEST_SCHEME", false);
4665 Rd->Host = Rd->headers->getstr(Rd->headers, "HTTP_HOST", false);
4666//char *SUri = Rd->headers->getstr(Rd->headers, "SCRIPT_URI", false);
4667 Rd->RUri = Rd->headers->getstr(Rd->headers, "REQUEST_URI", false);
4668//char *Cookies = Rd->headers->getstr(Rd->headers, "HTTP_COOKIE", false);
4669//char *Referer = Rd->headers->getstr(Rd->headers, "HTTP_REFERER", false);
4670//char *RAddr = Rd->headers->getstr(Rd->headers, "REMOTE_ADDR", false);
4671//char *Cache = Rd->headers->getstr(Rd->headers, "HTTP_CACHE_CONTROL", false);
4672//char *FAddrs = Rd->headers->getstr(Rd->headers, "FCGI_WEB_SERVER_ADDRS", false);
4673//char *Since = Rd->headers->getstr(Rd->headers, "IF_MODIFIED_SINCE", false);
4674 /* Per the spec CGI https://www.ietf.org/rfc/rfc3875
4675 meta-variable-name = "AUTH_TYPE" | "CONTENT_LENGTH" |
4676 "CONTENT_TYPE" | "GATEWAY_INTERFACE" |
4677 "PATH_INFO" | "PATH_TRANSLATED" |
4678 "QUERY_STRING" | "REMOTE_ADDR" |
4679 "REMOTE_HOST" | "REMOTE_IDENT" |
4680 "REMOTE_USER" | "REQUEST_METHOD" |
4681 "SCRIPT_NAME" | "SERVER_NAME" |
4682 "SERVER_PORT" | "SERVER_PROTOCOL" |
4683 "SERVER_SOFTWARE"
4684 Also protocol / scheme specific ones -
4685 HTTP_* comes from some of the request header. The rest are likely part of the other env variables.
4686 Also covers HTTPS headers, with the HTTP_* names.
4687
4688 */
4689
4690T("COOKIES");
4691 Rd->cookies = toknize(Rd->headers->getstr(Rd->headers, "HTTP_COOKIE", false), "=;");
4692 santize(Rd->cookies, TRUE);
4693T("QUERY");
4694 Rd->queries = toknize(Rd->headers->getstr(Rd->headers, "QUERY_STRING", false), "=&");
4695 santize(Rd->queries, TRUE);
4696 char *Body = NULL;
4697 if (Length != NULL)
4698 {
4699 size_t len = strtol(Length, NULL, 10);
4700 Body = xmalloc(len + 1);
4701 int c = FCGI_fread(Body, 1, len, FCGI_stdin);
4702 if (c != len)
4703 {
4704 E("Tried to read %d of the body, only got %d!", len, c);
4705 }
4706 Body[len] = '\0';
4707 }
4708T("BODY");
4709 Rd->body = toknize(Body, "=&");
4710 santize(Rd->body, TRUE);
4711
4712
4713 I("Started SledjChisl FCGI web %s request ROLE = %s, body is %s bytes, pid %d.", Rd->Method, Role, Length, getpid());
4714 I(" %s://%s%s -> %s/html%s", Rd->Scheme, Rd->Host, Rd->RUri, webRoot, Path);
4715
4716
4717/* TODO - other headers may include -
4718 different Content-type
4719 Status: 304 Not Modified
4720 Last-Modified: timedatemumble
4721 https://en.wikipedia.org/wiki/List_of_HTTP_header_fields
4722*/
4723 Rd->Rheaders->putstr(Rd->Rheaders, "Status", "200 OK");
4724 Rd->Rheaders->putstr(Rd->Rheaders, "Content-type", "text/html");
4725// TODO - check these.
4726 // This is all dynamic web pages, and likeley secure to.
4727 // Most of these are from https://www.smashingmagazine.com/2017/04/secure-web-app-http-headers/
4728 // https://www.twilio.com/blog/a-http-headers-for-the-responsible-developer is good to, and includes useful compression and image stuff.
4729 // On the other hand, .css files are referenced, which might be better off being cached, so should tweak some of thees.
4730 Rd->Rheaders->putstr(Rd->Rheaders, "Cache-Control", "no-cache, no-store, must-revalidate");
4731 Rd->Rheaders->putstr(Rd->Rheaders, "Pragma", "no-cache");
4732 Rd->Rheaders->putstr(Rd->Rheaders, "Expires", "-1");
4733// Rd->Rheaders->putstr(Rd->Rheaders, "Content-Security-Policy", "script-src 'self'"); // This can get complex.
4734 Rd->Rheaders->putstr(Rd->Rheaders, "X-XSS-Protection", "1;mode=block");
4735 Rd->Rheaders->putstr(Rd->Rheaders, "X-Frame-Options", "SAMEORIGIN");
4736 Rd->Rheaders->putstr(Rd->Rheaders, "X-Content-Type-Options", "nosniff");
4737// Failed experiment.
4738// Rd->Rheaders->putstr(Rd->Rheaders, "X-Toke-N-Munchie", "foo, bar");
4739
4740 if ((strcmp("GET", Rd->Method) != 0) && (strcmp("HEAD", Rd->Method) != 0) && (strcmp("POST", Rd->Method) != 0))
4741 {
4742 E("Unsupported HTTP method %s", Rd->Method);
4743 Rd->Rheaders->putstr(Rd->Rheaders, "Status", "405 Method Not Allowed");
4744 goto sendReply;
4745 }
4746
4747 memset(toybuf, 0, sizeof(toybuf));
4748 snprintf(toybuf, sizeof(toybuf), "%s/html%s", webRoot, Path);
4749 HTMLfile *thisFile = checkHTMLcache(toybuf);
4750 if (NULL == thisFile)
4751 {
4752 dynPage *dp = dynPages->get(dynPages, &Path[1], NULL, false);
4753 if (NULL == dp)
4754 {
4755 E("Can't access file %s", toybuf);
4756 Rd->Rheaders->putstr(Rd->Rheaders, "Status", "404 Not Found");
4757 E("Failed to open %s, it's not a virtual file either", toybuf);
4758 goto sendReply;
4759 }
4760 I("Dynamic page %s found.", dp->name);
4761 dp->func(toybuf, Rd, thisFile);
4762 char *finl = Rd->reply->tostring(Rd->reply); // This mallocs new storage and returns it to us.
4763// TODO - maybe cache this?
4764 qlist_t *fragments = fragize(finl, Rd->reply->datasize(Rd->reply));
4765 Rd->reply->free(Rd->reply);
4766 Rd->reply = qgrow(QGROW_THREADSAFE);
4767 unfragize(fragments, Rd);
4768goto sendReply;
4769 }
4770
4771 tmp0 = qfile_get_ext(toybuf);
4772 tmp1 = mimeTypes->getstr(mimeTypes, tmp0, false);
4773 if (NULL != tmp1)
4774 {
4775 if (strncmp("text/", tmp1, 5) != 0)
4776 {
4777 E("Only text formats are supported - %s", toybuf);
4778 Rd->Rheaders->putstr(Rd->Rheaders, "Status", "415 Unsupported Media Type");
4779 goto sendReply;
4780 }
4781 }
4782 else
4783 {
4784 E("Not actually a teapot, er I mean file has no extension, can't determine media type the easy way - %s", toybuf);
4785 Rd->Rheaders->putstr(Rd->Rheaders, "Status", "418 I'm a teapot");
4786 goto sendReply;
4787 }
4788
4789// Rd->Rheaders->putstr(Rd->Rheaders, "Last-Modified", thisFile->last.tv_sec);
4790// This is dynamic content, it's always gonna be modified. I think.
4791// if (NULL != Since)
4792// {
4793// time_t snc = qtime_parse_gmtstr(Since);
4794// TODO - should validate the time, log and ignore it if not valid.
4795// if (thisFile->last.tv_sec < snc)
4796// {
4797// D("Status: 304 Not Modified - %s", toybuf);
4798// setHeader("Status", "304 Not Modified");
4799// goto sendReply;
4800// }
4801// }
4802
4803 if (strcmp("HEAD", Rd->Method) == 0)
4804 goto sendReply;
4805
4806 getStats(database, stats);
4807 unfragize(thisFile->fragments, Rd);
4808
4809sendReply:
4810 /* Send headers.
4811 BTW, the Status header should be sent first I think.
4812 https://www.ietf.org/rfc/rfc3875 6.2 says order isn't important.
4813 It even says Status is optional, 200 is assumed. Content-Type is mandatory.
4814 8.2 "Recommendations for Scripts" is worth complying with.
4815 9 "Security Considerations"
4816 https://tools.ietf.org/html/rfc7230 3.1.2 says status line must be first. lol
4817 */
4818 FCGI_printf("Status: %s\r\n", getStrH(Rd->Rheaders, "Status"));
4819 memset((void *) &hobj, 0, sizeof(hobj));
4820 Rd->Rheaders->lock(Rd->Rheaders);
4821 while (Rd->Rheaders->getnext(Rd->Rheaders, &hobj, false) == true)
4822 {
4823 if (strcmp("Status", (char *) hobj.name) != 0)
4824 FCGI_printf("%s: %s\r\n", (char *) hobj.name, (char *) hobj.data);
4825 }
4826 Rd->Rheaders->unlock(Rd->Rheaders);
4827 // Send cookies.
4828 memset((void *) &hobj, 0, sizeof(hobj));
4829 Rd->Rcookies->lock(Rd->Rcookies);
4830 while (Rd->Rcookies->getnext(Rd->Rcookies, &hobj, false) == true)
4831 {
4832 cookie *ck = (cookie *) hobj.data;
4833 FCGI_printf("Set-Cookie: %s=%s", ck->cookie, ck->value);
4834// if (NULL != ck->expires) FCGI_printf("; Expires=%s", ck->expires);
4835 if (NULL != ck->domain) FCGI_printf("; Domain=%s", ck->domain);
4836 if (NULL != ck->path) FCGI_printf("; Path=%s", ck->path);
4837 if (0 != ck->maxAge) FCGI_printf("; Max-Age=%d", ck->maxAge);
4838 if ( ck->secure) FCGI_printf("; Secure");
4839 if ( ck->httpOnly) FCGI_printf("; HttpOnly");
4840 if (CS_STRICT == ck->site) FCGI_printf("; SameSite=Strict");
4841 if (CS_LAX == ck->site) FCGI_printf("; SameSite=Lax");
4842 if (CS_NONE == ck->site) FCGI_printf("; SameSite=None");
4843 FCGI_printf("\r\n");
4844 free(ck->value);
4845 free(ck->cookie);
4846 }
4847 FCGI_printf("\r\n");
4848 Rd->cookies->unlock(Rd->cookies);
4849 // Send body.
4850 char *final = Rd->reply->tostring(Rd->reply);
4851 if (NULL == final)
4852 {
4853 tmp0 = Rd->Rheaders->getstr(Rd->Rheaders, "Status", false);
4854 if (NULL == tmp0)
4855 {
4856 E("Some sort of error happpened! Status: UNKNOWN!!");
4857 FCGI_printf("Some sort of error happpened! Status: UNKNOWN!!");
4858 }
4859 else
4860 {
4861 E("Some sort of error happpened! Status: %s", tmp0);
4862 FCGI_printf("Some sort of error happpened! Status: %s", tmp0);
4863 }
4864 }
4865 else
4866 {
4867 FCGI_printf("%s", final);
4868 free(final);
4869 }
4870
4871fcgiDone:
4872 FCGI_Finish();
4873 qgrow_free(Rd->reply);
4874 qlist_free(Rd->messages);
4875 qlist_free(Rd->errors);
4876 qhashtbl_free(Rd->Rheaders);
4877 qhashtbl_free(Rd->Rcookies);
4878 qhashtbl_free(Rd->database);
4879 qhashtbl_free(Rd->stuff);
4880 qhashtbl_free(Rd->headers);
4881 qhashtbl_free(Rd->cookies);
4882 qhashtbl_free(Rd->body);
4883 qhashtbl_free(Rd->queries);
4884
4885 struct timespec now;
4886 if (-1 == clock_gettime(CLOCK_REALTIME, &now))
4887 perror_msg("Unable to get the time.");
4888 double n = (now.tv_sec * 1000000000.0) + now.tv_nsec;
4889 double t = (Rd->then.tv_sec * 1000000000.0) + Rd->then.tv_nsec;
4890 I("Finished SledjChisl web request, took %lf seconds", (n - t) / 1000000000.0);
4891 }
4892
4893 FCGI_fprintf(FCGI_stderr, "Stopped SledjChisl web server.\n");
4894 D("Stopped SledjChisl web server.");
4895
4896 goto finished;
4897 }
4898
4899
4900 if (!isTmux)
4901 { // Let's see if the proper tmux server is even running.
4902 memset(toybuf, 0, sizeof(toybuf));
4903 snprintf(toybuf, sizeof(toybuf), "%s %s/%s -q list-sessions 2>/dev/null | grep -q %s:", Tcmd, scRoot, Tsocket, Tconsole);
4904 i = system(toybuf);
4905 if (WIFEXITED(i))
4906 {
4907 if (0 != WEXITSTATUS(i)) // No such sesion, create it.
4908 {
4909 memset(toybuf, 0, sizeof(toybuf));
4910 // 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.
4911 // 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.
4912 snprintf(toybuf, sizeof(toybuf),
4913 "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'",
4914 scUser, Tcmd, scRoot, Tsocket, Tconsole, Ttab, Tconsole, scRoot);
4915 i = system(toybuf);
4916 if (!WIFEXITED(i))
4917 E("tmux new-session command failed!");
4918 }
4919 // Join the session.
4920 memset(toybuf, 0, sizeof(toybuf));
4921 snprintf(toybuf, sizeof(toybuf), "%s %s/%s select-window -t '%s' \\; attach-session -t '%s'", Tcmd, scRoot, Tsocket, Tconsole, Tconsole);
4922 i = system(toybuf);
4923 if (!WIFEXITED(i))
4924 E("tmux attach-session command failed!");
4925 goto finished;
4926 }
4927 else
4928 E("tmux list-sessions command failed!");
4929 }
4930
4931
4932
4933 simList *sims = getSims();
4934 if (1)
4935 {
4936 struct sysinfo info;
4937 float la;
4938
4939 sysinfo(&info);
4940 la = info.loads[0]/65536.0;
4941
4942 if (!checkSimIsRunning("ROBUST"))
4943 {
4944 char *d = xmprintf("%s.{right}", Ttab);
4945 char *c = xmprintf("cd %s/current/bin", scRoot);
4946
4947 I("ROBUST is starting up.");
4948 sendTmuxCmd(d, c);
4949 free(c);
4950 c = xmprintf("mono Robust.exe -inidirectory=%s/config/ROBUST", scRoot);
4951 sendTmuxCmd(d, c);
4952 free(c);
4953 waitTmuxText(d, "INITIALIZATION COMPLETE FOR ROBUST");
4954 I("ROBUST is done starting up.");
4955 la = waitLoadAverage(la, loadAverageInc / 3.0, simTimeOut / 3);
4956 free(d);
4957 }
4958
4959// for (i = 0; i < sims->num; i++)
4960 for (i = 0; i < 2; i++)
4961 {
4962 char *sim = sims->sims[i], *name = getSimName(sims->sims[i]);
4963
4964 if (!checkSimIsRunning(sim))
4965 {
4966 I("%s is starting up.", name);
4967 memset(toybuf, 0, sizeof(toybuf));
4968 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'",
4969 Tcmd, scRoot, Tsocket, name, Tconsole, i + 1, scRoot, scRoot, sim);
4970 int r = system(toybuf);
4971 if (!WIFEXITED(r))
4972 E("tmux new-window command failed!");
4973 else
4974 {
4975 memset(toybuf, 0, sizeof(toybuf));
4976 snprintf(toybuf, sizeof(toybuf), "INITIALIZATION COMPLETE FOR %s", name);
4977 waitTmuxText(name, toybuf);
4978 I("%s is done starting up.", name);
4979 la = waitLoadAverage(la, loadAverageInc, simTimeOut);
4980 }
4981 }
4982 }
4983
4984 }
4985 else if (!strcmp(cmd, "create")) // "create name x,y size"
4986 {
4987 }
4988 else if (!strcmp(cmd, "start")) // "start sim01" "start Welcome" "start" start everything
4989 {
4990 }
4991 else if (!strcmp(cmd, "backup")) // "backup onefang rejected" "backup sim01" "backup Welcome" "backup" backup everything
4992 { // If it's not a sim code, and not a sim name, it's an account inventory.
4993 }
4994 else if (!strcmp(cmd, "gitAR")) // "gitAR i name"
4995 {
4996 }
4997 else if (!strcmp(cmd, "stop")) // "stop sim01" "stop Welcome" "stop" stop everything
4998 {
4999 }
5000
5001
5002 double sum;
5003
5004 // Load the file containing the script we are going to run
5005 status = luaL_loadfile(L, "script.lua");
5006 if (status)
5007 {
5008 // If something went wrong, error message is at the top of the stack
5009 E("Couldn't load file: %s", lua_tostring(L, -1));
5010 goto finished;
5011 }
5012
5013 /*
5014 * Ok, now here we go: We pass data to the lua script on the stack.
5015 * That is, we first have to prepare Lua's virtual stack the way we
5016 * want the script to receive it, then ask Lua to run it.
5017 */
5018 lua_newtable(L); /* We will pass a table */
5019
5020 /*
5021 * To put values into the table, we first push the index, then the
5022 * value, and then call lua_rawset() with the index of the table in the
5023 * stack. Let's see why it's -3: In Lua, the value -1 always refers to
5024 * the top of the stack. When you create the table with lua_newtable(),
5025 * the table gets pushed into the top of the stack. When you push the
5026 * index and then the cell value, the stack looks like:
5027 *
5028 * <- [stack bottom] -- table, index, value [top]
5029 *
5030 * So the -1 will refer to the cell value, thus -3 is used to refer to
5031 * the table itself. Note that lua_rawset() pops the two last elements
5032 * of the stack, so that after it has been called, the table is at the
5033 * top of the stack.
5034 */
5035 for (i = 1; i <= 5; i++)
5036 {
5037 lua_pushnumber(L, i); // Push the table index
5038 lua_pushnumber(L, i*2); // Push the cell value
5039 lua_rawset(L, -3); // Stores the pair in the table
5040 }
5041
5042 // By what name is the script going to reference our table?
5043 lua_setglobal(L, "foo");
5044
5045 // Ask Lua to run our little script
5046 result = lua_pcall(L, 0, LUA_MULTRET, 0);
5047 if (result)
5048 {
5049 E("Failed to run script: %s", lua_tostring(L, -1));
5050 goto finished;
5051 }
5052
5053 // Get the returned value at the top of the stack (index -1)
5054 sum = lua_tonumber(L, -1);
5055
5056 I("Script returned: %.0f", sum);
5057
5058 lua_pop(L, 1); // Take the returned value out of the stack
5059
5060 // An example of calling a toy directly.
5061// printf("\n\n");
5062// char *argv[] = {"ls", "-l", "-a", NULL};
5063// printf("%d\n", runToy(argv));
5064
5065
5066 puts("");
5067 fflush(stdout);
5068
5069finished:
5070 if (database) mysql_close(database);
5071 mysql_library_end();
5072 lua_close(L);
5073 if (stats)
5074 {
5075 if (stats->stats) stats->stats->free(stats->stats);
5076 free(stats);
5077 }
5078 if (configs) configs->free(configs);
5079// return EXIT_SUCCESS;
5080}