diff options
author | onefang | 2020-03-16 20:22:12 +1000 |
---|---|---|
committer | onefang | 2020-03-16 20:22:12 +1000 |
commit | 3f67f0304f2138a38cf2695afec163b12db02d2c (patch) | |
tree | 3400e601150a5a8910b1e0f4aea8baf328f43d45 /src | |
parent | Shift web stuff to the www folder, and update it. (diff) | |
download | opensim-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.lua | 26 | ||||
-rwxr-xr-x | src/BuildIt.sh | 140 | ||||
-rw-r--r-- | src/miniconfig | 1 | ||||
-rw-r--r-- | src/sledjchisl/NOTES.txt | 503 | ||||
-rw-r--r-- | src/sledjchisl/README | 51 | ||||
-rw-r--r-- | src/sledjchisl/fcgi_SC.c | 13 | ||||
-rw-r--r-- | src/sledjchisl/fcgi_SC.h | 136 | ||||
-rw-r--r-- | src/sledjchisl/script.lua | 18 | ||||
-rw-r--r-- | src/sledjchisl/sledjchisl.c | 5080 |
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. | ||
4 | if type(jit) == 'table' then | ||
5 | io.write('.sledjChisl.conf.lua is being run by ' .. jit.version .. ' under ' .. jit.os .. ' on a ' .. jit.arch .. '\n') | ||
6 | else | ||
7 | io.write('.sledjChisl.conf.lua is being run by Lua version ' .. _VERSION .. '\n') | ||
8 | end | ||
9 | |||
10 | config = | ||
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 | } | ||
26 | return config | ||
diff --git a/src/BuildIt.sh b/src/BuildIt.sh index d199e95..2feb6f4 100755 --- a/src/BuildIt.sh +++ b/src/BuildIt.sh | |||
@@ -1,33 +1,133 @@ | |||
1 | #!/bin/bash | 1 | #!/bin/bash |
2 | 2 | ||
3 | mkdir -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. |
7 | if [ ! -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 | ||
11 | else | ||
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 | ||
16 | fi | ||
17 | |||
18 | if [ ! -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 | ||
28 | fi | ||
29 | |||
30 | echo "" | ||
31 | echo "" | ||
32 | echo "" | ||
33 | |||
34 | if [ ! -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 | ||
42 | else | ||
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 | ||
47 | fi | ||
48 | |||
49 | if [ ! -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 | ||
57 | fi | ||
58 | |||
59 | echo "" | ||
60 | echo "" | ||
61 | echo "" | ||
62 | |||
63 | if [ ! -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 | ||
67 | else | ||
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 | ||
72 | fi | ||
73 | |||
74 | if [ ! -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 | ||
83 | fi | ||
84 | |||
85 | echo "" | ||
86 | echo "" | ||
87 | echo "" | ||
88 | |||
4 | if [ ! -d git-sub-modules/toybox ]; then | 89 | if [ ! -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 |
8 | else | 93 | else |
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 | ||
12 | fi | 98 | fi |
13 | 99 | ||
14 | pushd git-sub-modules/toybox | 100 | if [ ! -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 |
18 | popd | 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 | ||
20 | mkdir -p build | 110 | pushd build/toybox >/dev/null |
21 | rm -fr build/toybox | 111 | sed -e "s/strend(/tb_strend(/g" -i lib/lib.h |
22 | cp -r git-sub-modules/toybox build/ | 112 | find ./ -type f -name "*.c" -exec sed -e "s/strend(/tb_strend(/g" -i {} \; |
23 | ln -fs ../../../boxes build/toybox/toys/boxes | 113 | make clean |
24 | ln -fs ../toys/boxes/handlekeys.c build/toybox/lib | 114 | #make defconfig |
25 | ln -fs ../toys/boxes/handlekeys.h build/toybox/lib | 115 | #make menuconfig |
116 | make allnoconfig KCONFIG_ALLCONFIG=../../miniconfig || exit 1 | ||
117 | popd >/dev/null | ||
118 | fi | ||
119 | |||
120 | echo "" | ||
121 | echo "" | ||
122 | echo "" | ||
26 | 123 | ||
27 | pushd build/toybox >/dev/null | 124 | pushd build/toybox >/dev/null |
28 | make clean | 125 | export CFLAGS="-I../luajit/src -I../fcgi2 -I../fcgi2/include -I../qlibc/include/qlibc $(mysql_config --cflags) -g3" |
29 | #make defconfig | 126 | export LDFLAGS="-L../luajit/src -L../fcgi2/libfcgi/.libs -L../qlibc/lib $(mysql_config --libs) -Wl,-E -l:libluajit.a -l:libqlibcext.a -l:libfcgi.a -l:libqlibc.a -lcrypto -luuid" |
30 | ##make menuconfig | ||
31 | make allnoconfig KCONFIG_ALLCONFIG=../../miniconfig || exit 1 | ||
32 | make || exit 1 | 127 | make || exit 1 |
33 | popd >/dev/null | 128 | popd >/dev/null |
129 | |||
130 | |||
131 | sudo rm -f /opt/opensim_SC/caches/sledjchisl.socket | ||
132 | sudo spawn-fcgi -n -u opensimsc -s /opt/opensim_SC/caches/sledjchisl.socket -M 0660 -G www-data -- /usr/bin/valgrind build/toybox/generated/unstripped/toybox sledjchisl | ||
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 | |||
5 | CONFIG_SH=y | 5 | CONFIG_SH=y |
6 | CONFIG_SU=y | 6 | CONFIG_SU=y |
7 | CONFIG_UUIDGEN=y | 7 | CONFIG_UUIDGEN=y |
8 | CONFIG_SLEDJCHISL=y | ||
8 | CONFIG_TOYBOX_FREE=y | 9 | CONFIG_TOYBOX_FREE=y |
9 | CONFIG_TOYBOX_HELP=y | 10 | CONFIG_TOYBOX_HELP=y |
10 | CONFIG_TOYBOX_HELP_DASHDASH=y | 11 | CONFIG_TOYBOX_HELP_DASHDASH=y |
diff --git a/src/sledjchisl/NOTES.txt b/src/sledjchisl/NOTES.txt new file mode 100644 index 0000000..a5f0861 --- /dev/null +++ b/src/sledjchisl/NOTES.txt | |||
@@ -0,0 +1,503 @@ | |||
1 | I'm re-purposing this for SledjHamr https://sledjhamr.org/git/docs/index.html | ||
2 | |||
3 | The general structure of SledjHamr is a bunch of servers talking to each | ||
4 | other via Internet (or just local) connections. One of them is a web | ||
5 | server for assets, world data, and inventory. | ||
6 | |||
7 | Originally I didn't think using a web based world client was a good idea, | ||
8 | however it might be better to have one, for reasons. Now I need a web | ||
9 | management console that can do all the things the current tmux console | ||
10 | can, including OpenSim console and commands. Plus account management for | ||
11 | users. I can also use a web based Jabber / XMPP front end to chat, IM, | ||
12 | and group chatter, which would run in the normal viewers web browser. | ||
13 | This provides a doorway into putting SledjHamr stuff in existing viewers | ||
14 | without needing them to support it. So a web based viewer now makes more | ||
15 | sense, and also means we can get away with not needing a viewer at all. | ||
16 | |||
17 | Toybox itself doesn't include a web server, and I don't think there is | ||
18 | one on the roadmap. So we have to use an external web server, which was | ||
19 | a design goal of SledjHamr in the first place, using existing mature | ||
20 | HTTP infrastructure, coz that's already solved problems for a bunch of | ||
21 | things that plague OS/SL to this day. Clear your cache! Pffft. | ||
22 | |||
23 | So sledjchisl.c will be the "love world server", though initially it just | ||
24 | drives OpenSim_SC in tmux via tmux commands to send keys and read output. | ||
25 | Later it might run opensim_SC directly and use STDIN and STDOUT to do | ||
26 | everything. It'll also provide the text management front end that runs | ||
27 | in the left tmux panel of the first window, which is why it's based on | ||
28 | boxes in the first place. Later still it can take over opensim_SC | ||
29 | functions as I move them out of mono. | ||
30 | |||
31 | We will need a text, web, and GUI version of this management front end. | ||
32 | Hmmm, maybe don't need a GUI version, GUI users can just run a terminal. | ||
33 | |||
34 | After much research, FastCGI / FCGI seems to be the most portable way of | ||
35 | interfacing with existing web servers. FCGI protocol closes STDERR and | ||
36 | STDOUT, and uses STDIN as it's two way communications channel to the web | ||
37 | server, so our FCGI module can't be used as the text management front | ||
38 | end. This is probably a good idea to keep them seperate anyway, for | ||
39 | security, coz the web server is exposed to the world, the console isn't. | ||
40 | |||
41 | Currently sledjchisl.c tests to see if it's running in tmux already, if | ||
42 | it isn't it starts up tmux runs itself into this new tmux, then exits. | ||
43 | So it could also test if it's running from FCGI, and switch to web mode, | ||
44 | then it'll need to find the tmuxed instance to send commands to it. | ||
45 | Either via nails connection, or sending tmux commands via shell. | ||
46 | |||
47 | FCGI has methods of dealing with auth and templates. B-) | ||
48 | |||
49 | So for now I think I'll have the text and web management front ends in | ||
50 | sledjchisl.c, and the love world server as well. I can split them up | ||
51 | later if I need to. | ||
52 | |||
53 | |||
54 | |||
55 | |||
56 | I has Apache 2.4.25-3+deb9u9 | ||
57 | MariaDB 10.1.44-MariaDB | ||
58 | |||
59 | |||
60 | https://gist.github.com/dermesser/e2f9b66457ae19ebd116 | ||
61 | Multithreaded example in C. | ||
62 | |||
63 | |||
64 | ------------------------------------------------------------------- | ||
65 | |||
66 | Apache doesn't seem to support FCGI filter role, so I might have to do | ||
67 | without. Might be better anyway. | ||
68 | |||
69 | |||
70 | "A Filter is similar in functionality to a Responder that takes a data | ||
71 | file as a parameter. The difference is that with a Filter, both the data | ||
72 | file and the Filter itself can be access controlled using the Web | ||
73 | server's access control mechanisms, while a Responder that takes the name | ||
74 | of a data file as a parameter must perform its own access control checks | ||
75 | on 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 | |||
83 | RESPONDER | ||
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 | |||
92 | FILTER | ||
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 | |||
105 | Soooo, FILTER might be slower anyway if we are caching the filtered file, | ||
106 | or mmapping it, coz filter has to start sending the filtered file, even | ||
107 | if it's to be served from cache. Plus no need to wait for FCGI_STDIN | ||
108 | before spewing it out. | ||
109 | |||
110 | |||
111 | Last 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 | |||
143 | Account creation process in the database. | ||
144 | |||
145 | Apart from the usual input validation of things... | ||
146 | |||
147 | |||
148 | OpenSim/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 | |||
154 | OpenSim/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 | |||
163 | OpenSim/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 | |||
175 | account = 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 | |||
198 | success = 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 | |||
209 | gridUserService.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 | |||
218 | success = 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 | |||
249 | CreateDefaultAppearanceEntries(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 | |||
262 | UserAccounts 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. | ||
269 | Presence table - | ||
270 | UserID varchar(255) | ||
271 | RegionID char(36) | ||
272 | SessionID char(36) | ||
273 | SecureSessionID char(36) | ||
274 | LastSeen timestamp | ||
275 | tokens 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 | ||
279 | userdata (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) | ||
284 | auth.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 | |||
287 | PLAN- | ||
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 | |||
297 | create -> 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 | |||
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 | |||
397 | https://project-awesome.org/aleksandar-todorovic/awesome-c | ||
398 | A curated list of C good stuff. | ||
399 | |||
400 | https://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 | |||
405 | https://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 | |||
414 | apt install libmariadbclient-dev libapache2-mod-fcgid | ||
415 | |||
416 | ------------------------------------------------------------------- | ||
417 | |||
418 | Merge 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 | |||
436 | https://medium.com/altcampus/how-to-merge-two-or-multiple-git-repositories-into-one-9f8a5209913f | ||
437 | https://medium.com/@checko/merging-two-git-repositories-into-one-preserving-the-git-history-4e20d3fafa4e | ||
438 | https://thoughts.t37.net/merging-2-different-git-repositories-without-losing-your-history-de7a06bba804 | ||
439 | https://blog.doismellburning.co.uk/merging-two-git-repositories/ | ||
440 | |||
441 | Other much more complicated variations. | ||
442 | |||
443 | http://mbork.pl/2019-08-19_Transplanting_a_directory_to_another_Git_repository | ||
444 | |||
445 | |||
446 | ------------------------------------------------------------------- | ||
447 | |||
448 | Install / update / upgrade. | ||
449 | |||
450 | I 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 | |||
456 | Note that we are currently using the LuaJIT 2.1.0-beta3 branch of the | ||
457 | main Luajit repo. Everything else is on their master branches. | ||
458 | |||
459 | Bootstrap - | ||
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 | |||
477 | Install - | ||
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 | |||
488 | Update / 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 | |||
501 | Yeah I hate things that have their own packaging system, for needing to | ||
502 | step outside the operating systems packaging system, and adding to the too | ||
503 | long list of stuff I have to deal with manually, and now I are one. lol | ||
diff --git a/src/sledjchisl/README b/src/sledjchisl/README new file mode 100644 index 0000000..5d0a7b1 --- /dev/null +++ b/src/sledjchisl/README | |||
@@ -0,0 +1,51 @@ | |||
1 | I'm re-purposing this for SledjHamr https://sledjhamr.org/git/docs/index.html | ||
2 | |||
3 | The general structure of SledjHamr is a bunch of servers talking to each | ||
4 | other via Internet (or just local) connections. One of them is a web | ||
5 | server for assets, world data, and inventory. | ||
6 | |||
7 | Originally I didn't think using a web based world client was a good idea, | ||
8 | however it might be better to have one, for reasons. Now I need a web | ||
9 | management console that can do all the things the current tmux console | ||
10 | can, including OpenSim console and commands. Plus account management for | ||
11 | users. I can also use a web based Jabber / XMPP front end to chat, IM, | ||
12 | and group chatter, which would run in the normal viewers web browser. | ||
13 | This provides a doorway into putting SledjHamr stuff in existing viewers | ||
14 | without needing them to support it. So a web based viewer now makes more | ||
15 | sense, and also means we can get away with not needing a viewer at all. | ||
16 | |||
17 | Toybox itself doesn't include a web server, and I don't think there is | ||
18 | one on the roadmap. So we have to use an external web server, which was | ||
19 | a design goal of SledjHamr in the first place, using existing mature | ||
20 | HTTP infrastructure, coz that's already solved problems for a bunch of | ||
21 | things that plague OS/SL to this day. Clear your cache! Pffft. | ||
22 | |||
23 | So sledjchisl.c will be the "love world server", though initially it just | ||
24 | drives OpenSim_SC in tmux via tmux commands to send keys and read output. | ||
25 | Later it might run opensim_SC directly and use STDIN and STDOUT to do | ||
26 | everything. It'll also provide the text management front end that runs | ||
27 | in the left tmux panel of the first window, which is why it's based on | ||
28 | boxes in the first place. Later still it can take over opensim_SC | ||
29 | functions as I move them out of mono. | ||
30 | |||
31 | We will need a text, web, and GUI version of this management front end. | ||
32 | Hmmm, maybe don't need a GUI version, GUI users can just run a terminal. | ||
33 | |||
34 | After much research, FastCGI / FCGI seems to be the most portable way of | ||
35 | interfacing with existing web servers. FCGI protocol closes STDERR and | ||
36 | STDOUT, and uses STDIN as it's two way communications channel to the web | ||
37 | server, so our FCGI module can't be used as the text management front | ||
38 | end. This is probably a good idea to keep them seperate anyway, for | ||
39 | security, coz the web server is exposed to the world, the console isn't. | ||
40 | |||
41 | Currently sledjchisl.c tests to see if it's running in tmux already, if | ||
42 | it isn't it starts up tmux runs itself into this new tmux, then exits. | ||
43 | So it could also test if it's running from FCGI, and switch to web mode, | ||
44 | then it'll need to find the tmuxed instance to send commands to it. | ||
45 | Either via nails connection, or sending tmux commands via shell. | ||
46 | |||
47 | FCGI has methods of dealing with auth and templates. B-) | ||
48 | |||
49 | So for now I think I'll have the text and web management front ends in | ||
50 | sledjchisl.c, and the love world server as well. I can split them up | ||
51 | later if I need to. | ||
diff --git a/src/sledjchisl/fcgi_SC.c b/src/sledjchisl/fcgi_SC.c new file mode 100644 index 0000000..36aba77 --- /dev/null +++ b/src/sledjchisl/fcgi_SC.c | |||
@@ -0,0 +1,13 @@ | |||
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 | |||
6 | enum fcgiEventType{ | ||
7 | FSC_CSI, | ||
8 | FSC_KEYS, | ||
9 | FSC_MOUSE, | ||
10 | FSC_RAW | ||
11 | }; | ||
12 | |||
13 | struct 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 | |||
31 | typedef 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 | |||
74 | typedef struct { | ||
75 | unsigned char roleB1; | ||
76 | unsigned char roleB0; | ||
77 | unsigned char flags; | ||
78 | unsigned char reserved[5]; | ||
79 | } FCGI_BeginRequestBody; | ||
80 | |||
81 | typedef 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 | |||
98 | typedef 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 | |||
107 | typedef 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 | |||
127 | typedef struct { | ||
128 | unsigned char type; | ||
129 | unsigned char reserved[7]; | ||
130 | } FCGI_UnknownTypeBody; | ||
131 | |||
132 | typedef 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. | ||
4 | if type(jit) == 'table' then | ||
5 | io.write('script.lua is being run by ' .. jit.version .. ' under ' .. jit.os .. ' on a ' .. jit.arch .. '\n') | ||
6 | else | ||
7 | io.write('script.lua is being run by Lua version ' .. _VERSION .. '\n') | ||
8 | end | ||
9 | |||
10 | -- Receives a table, returns the sum of its components. | ||
11 | io.write("The table the script received has:\n"); | ||
12 | x = 0 | ||
13 | for i = 1, #foo do | ||
14 | print(i, foo[i]) | ||
15 | x = x + foo[i] | ||
16 | end | ||
17 | io.write("Returning data back to C\n"); | ||
18 | return x | ||
diff --git a/src/sledjchisl/sledjchisl.c b/src/sledjchisl/sledjchisl.c new file mode 100644 index 0000000..b5dfc97 --- /dev/null +++ b/src/sledjchisl/sledjchisl.c | |||
@@ -0,0 +1,5080 @@ | |||
1 | /* sledjchisl.c - opensim-SC management system. | ||
2 | * | ||
3 | * Copyright 2020 David Seikel <sledjchisl@sledjhamr.org> | ||
4 | * Not in SUSv4. An entirely new invention, thus no web site either. | ||
5 | |||
6 | USE_SLEDJCHISL(NEWTOY(sledjchisl, "m(mode):", TOYFLAG_USR|TOYFLAG_BIN)) | ||
7 | |||
8 | config SLEDJCHISL | ||
9 | bool "sledjchisl" | ||
10 | default y | ||
11 | help | ||
12 | usage: sledjchisl [-m|--mode mode] | ||
13 | |||
14 | opensim-SC management system. | ||
15 | */ | ||
16 | |||
17 | |||
18 | // TODO - figure out how to automate testing of this. | ||
19 | // Being all interactive and involving external web servers / viewers makes it hard. | ||
20 | |||
21 | // TODO - do all the setup on first run, and check if needed on each start up, to be self healing. | ||
22 | |||
23 | // TODO - 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 | ||
52 | extern 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 | |||
89 | GLOBALS( | ||
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 | */ | ||
130 | char *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 | */ | ||
192 | size_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 | ||
250 | int 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 | ||
284 | typedef 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> | ||
299 | int getrandom(void *b, size_t l, unsigned int f) | ||
300 | { | ||
301 | return (int) syscall(SYS_getrandom, b, l, f); | ||
302 | } | ||
303 | |||
304 | |||
305 | |||
306 | typedef struct _gridStats gridStats; | ||
307 | struct _gridStats | ||
308 | { | ||
309 | float next; | ||
310 | struct timeval last; | ||
311 | qhashtbl_t *stats; | ||
312 | }; | ||
313 | |||
314 | |||
315 | typedef struct _HTMLfile HTMLfile; | ||
316 | struct _HTMLfile | ||
317 | { | ||
318 | struct timespec last; | ||
319 | qlist_t *fragments; | ||
320 | }; | ||
321 | qhashtbl_t *HTMLfileCache = NULL; | ||
322 | |||
323 | |||
324 | typedef struct _reqData reqData; | ||
325 | |||
326 | |||
327 | typedef int (*fieldValidFunc) (reqData *Rd, qhashtbl_t *data); | ||
328 | typedef struct _validFunc validFunc; | ||
329 | struct _validFunc | ||
330 | { | ||
331 | char *name; | ||
332 | fieldValidFunc func; | ||
333 | }; | ||
334 | qlisttbl_t *fieldValidFuncs = NULL; | ||
335 | static 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 | |||
342 | typedef void *(*pageFunction) (char *file, reqData *Rd, HTMLfile *thisFile); | ||
343 | typedef struct _dynPage dynPage; | ||
344 | struct _dynPage | ||
345 | { | ||
346 | char *name; | ||
347 | pageFunction func; | ||
348 | }; | ||
349 | qhashtbl_t *dynPages; | ||
350 | static 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 | |||
357 | typedef void *(*pageBuildFunction) (reqData *Rd, char *message); | ||
358 | typedef struct _buildPage buildPage; | ||
359 | struct _buildPage | ||
360 | { | ||
361 | char *name; | ||
362 | pageBuildFunction func, eFunc; | ||
363 | }; | ||
364 | qhashtbl_t *buildPages; | ||
365 | static 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 | |||
380 | Also including values from the database. | ||
381 | |||
382 | URL query Values actually provided by the user in the FORM, and other things. | ||
383 | POST body Values actually provided by the user in the FORM. | ||
384 | cookies | ||
385 | https://stackoverflow.com/questions/4056306/how-to-handle-multiple-cookies-with-the-same-name | ||
386 | |||
387 | headers includes HTTP_COOKIE and QUERY_STRING | ||
388 | env includes headers and HTTP_COOKIE and QUERY_STRING | ||
389 | |||
390 | database 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 | |||
393 | We don't actually get the headers directly, it's all sent via the env. | ||
394 | |||
395 | http://docs.gantry.org/gantry4/advanced/setby | ||
396 | Says that query overrides cookies, but that might be just for their platform. | ||
397 | |||
398 | https://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 | |||
402 | Sending cookie headers is a special case, multiples can be sent, otherwise headers are singletons, only send one for each name. | ||
403 | |||
404 | local 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 | ||
409 | typedef struct _sesh sesh; | ||
410 | struct _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 | |||
419 | struct _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 | |||
434 | static 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, " salt = %s\n", shs->salt); | ||
442 | reply->addstrf(reply, " seshID = %s\n", shs->seshID); | ||
443 | reply->addstrf(reply, " timeStamp = %ld.%ld\n", shs->timeStamp[1].tv_sec, shs->timeStamp[1].tv_nsec); | ||
444 | reply->addstrf(reply, " sesh = %s\n", shs->sesh); | ||
445 | reply->addstrf(reply, " munchie = %s\n", shs->munchie); | ||
446 | reply->addstrf(reply, " toke_n_munchie = %s\n", shs->toke_n_munchie); | ||
447 | reply->addstrf(reply, " hashish = %s\n", shs->hashish); | ||
448 | reply->addstrf(reply, " leaf = %s\n", shs->leaf); | ||
449 | reply->addstr(reply, "</pre>\n"); | ||
450 | } | ||
451 | |||
452 | |||
453 | char toybuf[4096]; | ||
454 | boolean isTmux = 0; | ||
455 | boolean isWeb = 0; | ||
456 | char *pwd = ""; | ||
457 | char *scRoot = "/opt/opensim_SC"; | ||
458 | char *scUser = "opensimsc"; | ||
459 | char *Tconsole = "SledjChisl"; | ||
460 | char *Tsocket = "caches/opensim-tmux.socket"; | ||
461 | char *Ttab = "SC"; | ||
462 | char *Tcmd = "tmux -S"; | ||
463 | char *webRoot = "/opt/opensim_SC/web"; | ||
464 | char *URL = "fcgi-bin/sledjchisl.fcgi"; | ||
465 | int seshTimeOut = 30 * 60; | ||
466 | int idleTimeOut = 24 * 60 * 60; | ||
467 | int newbieTimeOut = 30; | ||
468 | float loadAverageInc = 0.5; | ||
469 | int simTimeOut = 45; | ||
470 | qhashtbl_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 | ||
479 | char *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 | ||
490 | void 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 | |||
526 | static void addStrL(qlist_t *list, char *s) | ||
527 | { | ||
528 | list->addlast(list, s, strlen(s) + 1); | ||
529 | } | ||
530 | |||
531 | static 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 | |||
542 | char *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 | |||
559 | char *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. | ||
573 | void 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 | |||
591 | int 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 | |||
603 | int 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 | |||
615 | void 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 | |||
636 | float 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. | ||
662 | struct dirtree *dirtree_handle_callback(struct dirtree *new, int (*callback)(struct dirtree *node)); | ||
663 | |||
664 | typedef struct _simList simList; | ||
665 | struct _simList | ||
666 | { | ||
667 | int len, num; | ||
668 | char **sims; | ||
669 | }; | ||
670 | |||
671 | static 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 | |||
689 | simList *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 | |||
703 | static 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 | |||
715 | char *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. | ||
763 | int 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 | |||
817 | static 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 | |||
825 | static void printEnv(char **envp) | ||
826 | { | ||
827 | for ( ; *envp != NULL; envp++) | ||
828 | D("%s", *envp); | ||
829 | } | ||
830 | |||
831 | |||
832 | typedef struct _rowData rowData; | ||
833 | struct _rowData | ||
834 | { | ||
835 | char **fieldNames; | ||
836 | qlist_t *rows; | ||
837 | }; | ||
838 | |||
839 | static 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 | |||
850 | static 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. | ||
863 | http://karlssonondatabases.blogspot.com/2010/07/prepared-statements-are-they-useful-or.html | ||
864 | https://blog.cotten.io/a-taste-of-mysql-in-c-87c5de84a31d?gi=ab3dd1425b29 | ||
865 | https://raspberry-projects.com/pi/programming-in-c/databases-programming-in-c/mysql/accessing-the-database | ||
866 | |||
867 | IG and CG now both have sims connected to other grids, so some sort of | ||
868 | multi database solution would be good, then we can run the grid and the | ||
869 | external sims all in one. | ||
870 | |||
871 | Not sure if this'll work with Count(*). | ||
872 | |||
873 | --------------------------------------------- | ||
874 | |||
875 | The complicated bit is the binds. | ||
876 | |||
877 | You are binding field values to C memory locations. | ||
878 | The parameters and returned fields need binds. | ||
879 | Mostly seems to be the value parts of the SQL statements. | ||
880 | |||
881 | I 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 | |||
890 | typedef struct _dbField dbField; | ||
891 | struct _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 | |||
900 | qlisttbl_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 | |||
912 | D("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 | |||
953 | void 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 | |||
967 | typedef struct _dbRequest dbRequest; | ||
968 | struct _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 | |||
982 | void 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 | |||
1036 | D("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 | { | ||
1172 | I("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 | |||
1453 | D("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 | |||
1592 | freeIt: | ||
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 | |||
1598 | end: | ||
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. | ||
1610 | void 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 | /* | ||
1630 | void 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 | |||
1663 | my_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 | |||
1705 | my_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 | |||
1746 | MYSQL_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 | |||
1792 | void replaceStr(qhashtbl_t *ssi, char *key, char *value) | ||
1793 | { | ||
1794 | ssi->putstr(ssi, key, value); | ||
1795 | } | ||
1796 | |||
1797 | void 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 | |||
1806 | float 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 | |||
1831 | gridStats *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 | |||
1942 | qhashtbl_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); | ||
1962 | D(" %s = %s", qstrtrim_head(key), val); | ||
1963 | val = NULL; | ||
1964 | } | ||
1965 | } | ||
1966 | if (NULL != val) | ||
1967 | { | ||
1968 | ret->putstr(ret, qstrtrim_head(key), val); | ||
1969 | D(" %s = %s", qstrtrim_head(key), val); | ||
1970 | } | ||
1971 | free(txt); | ||
1972 | return ret; | ||
1973 | } | ||
1974 | |||
1975 | void 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 | |||
2004 | void 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, " %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 | ||
2018 | enum cookieSame | ||
2019 | { | ||
2020 | CS_NOT, | ||
2021 | CS_STRICT, | ||
2022 | CS_LAX, // Apparently the default set by browsers these days. | ||
2023 | CS_NONE | ||
2024 | }; | ||
2025 | typedef struct _cookie cookie; | ||
2026 | struct _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 | |||
2035 | cookie *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 | |||
2059 | char *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 | |||
2070 | void 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, " %s = %s\n", obj.name, ((cookie *) obj.data)->value); | ||
2078 | tbl->unlock(tbl); | ||
2079 | reply->addstr(reply, "</pre>\n"); | ||
2080 | } | ||
2081 | |||
2082 | |||
2083 | |||
2084 | enum fragmentType | ||
2085 | { | ||
2086 | FT_TEXT, | ||
2087 | FT_PARAM, | ||
2088 | FT_LUA | ||
2089 | }; | ||
2090 | |||
2091 | typedef struct _fragment fragment; | ||
2092 | struct _fragment | ||
2093 | { | ||
2094 | enum fragmentType type; | ||
2095 | int length; | ||
2096 | char *text; | ||
2097 | }; | ||
2098 | |||
2099 | static 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 | |||
2123 | static 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 | |||
2140 | static 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 | |||
2204 | static 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 | |||
2209 | static 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 | } | ||
2215 | static void HTMLformEnd(qgrow_t *reply) | ||
2216 | { | ||
2217 | reply->addstr(reply, " </form>\n"); | ||
2218 | } | ||
2219 | |||
2220 | static 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 | |||
2228 | static 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 | |||
2242 | static 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 | } | ||
2249 | static void HTMLselectEnd(qgrow_t *reply) | ||
2250 | { | ||
2251 | reply->addstr(reply, " </select>\n </p>\n"); | ||
2252 | } | ||
2253 | static void HTMLselectEndNo(qgrow_t *reply) | ||
2254 | { | ||
2255 | reply->addstr(reply, " </select>"); | ||
2256 | } | ||
2257 | |||
2258 | static 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 | |||
2267 | static 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 | |||
2272 | static 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 | |||
2285 | static int count = 0; | ||
2286 | void 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 | |||
2337 | static void HTMLfooter(qgrow_t *reply) | ||
2338 | { | ||
2339 | reply->addstr(reply, | ||
2340 | " </font>" | ||
2341 | " </body>\n</html>\n"); | ||
2342 | } | ||
2343 | |||
2344 | |||
2345 | fragment *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 | |||
2356 | qlist_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 | |||
2414 | void 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 | |||
2432 | HTMLfile *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 | |||
2608 | https://owasp.org/www-project-cheat-sheets/cheatsheets/Input_Validation_Cheat_Sheet.html#Email_Address_Validation | ||
2609 | https://cheatsheetseries.owasp.org/ | ||
2610 | https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html | ||
2611 | https://owasp.org/www-project-cheat-sheets/cheatsheets/Authentication_Cheat_Sheet.html | ||
2612 | https://softwareengineering.stackexchange.com/questions/46716/what-technical-details-should-a-programmer-of-a-web-application-consider-before | ||
2613 | https://wiki.owasp.org/index.php/OWASP_Guide_Project | ||
2614 | https://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. | ||
2620 | void 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 | |||
2626 | Large 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. | ||
2629 | Cryptographically 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. | ||
2633 | A 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. | ||
2640 | Double 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 | |||
2650 | SOOOOO - 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 | |||
2681 | I should make it easy to change the HMAC() function. Less important for these short lived sessions, more important for the linky URLs, most important for stared password hashes. | ||
2682 | Same for the pepper. | ||
2683 | |||
2684 | The 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 | |||
2687 | NOTE - storing a pepper on the same RAID array as everything else will be a problem when it comes time to replace one of the disks. | ||
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 | |||
2691 | https://stackoverflow.com/questions/16891729/best-practices-salting-peppering-passwords | ||
2692 | */ | ||
2693 | |||
2694 | static 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 | |||
2753 | static 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 | |||
2819 | static 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 | |||
2887 | static 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." | ||
2894 | https://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. | ||
2897 | I 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 | */ | ||
2900 | static 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 | } | ||
2906 | static 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 | |||
2914 | W("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 | |||
2981 | char *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 | |||
2997 | boolean 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 | |||
3007 | boolean 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 | |||
3025 | char *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 | |||
3043 | int 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 | |||
3110 | static 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 | { | ||
3243 | W("Validated session."); | ||
3244 | sesh *shs = &Rd->shs; | ||
3245 | |||
3246 | qstrcpy(shs->leaf, sizeof(shs->leaf), leaf); | ||
3247 | if (linky) | ||
3248 | { | ||
3249 | W("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 | |||
3295 | static 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 | |||
3345 | static 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 | |||
3383 | static 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 | |||
3411 | static 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 | { | ||
3543 | if (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 | |||
3619 | static 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 | |||
3816 | static 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 | |||
3926 | int 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 | |||
3942 | W("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 | |||
3954 | void 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 | |||
3981 | void 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 | |||
4051 | void 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 | |||
4089 | void 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 | |||
4119 | void 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 | |||
4291 | void 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 - | ||
4379 | To change or extend the list of standard libraries to load, copy | ||
4380 | src/lib_init.c to your project and modify it accordingly. Make sure the | ||
4381 | jit 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 "-" -> "_". | ||
4639 | T("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); | ||
4650 | if ((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 | |||
4690 | T("COOKIES"); | ||
4691 | Rd->cookies = toknize(Rd->headers->getstr(Rd->headers, "HTTP_COOKIE", false), "=;"); | ||
4692 | santize(Rd->cookies, TRUE); | ||
4693 | T("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 | } | ||
4708 | T("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); | ||
4768 | goto 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 | |||
4809 | sendReply: | ||
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 | |||
4871 | fcgiDone: | ||
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 | |||
5069 | finished: | ||
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 | } | ||