aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/src
diff options
context:
space:
mode:
authoronefang2020-09-08 21:34:54 +1000
committeronefang2020-09-08 21:34:54 +1000
commitbd58d3012c26d16150f650c389d1136741d3939d (patch)
tree3d2aec8bfe0a0c9061ddb4814e71aff33b9b8dc8 /src
parentAdd the old bash scripts. (diff)
downloadopensim-SC-bd58d3012c26d16150f650c389d1136741d3939d.zip
opensim-SC-bd58d3012c26d16150f650c389d1136741d3939d.tar.gz
opensim-SC-bd58d3012c26d16150f650c389d1136741d3939d.tar.bz2
opensim-SC-bd58d3012c26d16150f650c389d1136741d3939d.tar.xz
Add the SledjChisl stuff.
Diffstat (limited to 'src')
-rw-r--r--src/.sledjChisl.conf.lua58
-rwxr-xr-xsrc/BuildIt.sh132
-rw-r--r--src/boxes/BOXES.txt1130
-rw-r--r--src/boxes/BUGS.txt6
-rw-r--r--src/boxes/README34
-rw-r--r--src/boxes/boxes.c2517
-rw-r--r--src/boxes/dumbsh.c283
-rw-r--r--src/boxes/handlekeys.c445
-rw-r--r--src/boxes/handlekeys.h76
-rw-r--r--src/boxes/showkey.c149
m---------src/build/fcgi20
m---------src/build/luajit0
m---------src/build/qlibc0
m---------src/build/toybox0
-rw-r--r--src/git-sub-modules/README3
m---------src/git-sub-modules/fcgi20
m---------src/git-sub-modules/luajit0
m---------src/git-sub-modules/qlibc0
m---------src/git-sub-modules/toybox0
-rw-r--r--src/miniconfig13
-rw-r--r--src/sledjchisl/README118
-rw-r--r--src/sledjchisl/fcgi_SC.c13
-rw-r--r--src/sledjchisl/fcgi_SC.h136
-rw-r--r--src/sledjchisl/script.lua18
-rw-r--r--src/sledjchisl/sledjchisl.c7342
-rwxr-xr-xsrc/tickle.lua26
26 files changed, 12499 insertions, 0 deletions
diff --git a/src/.sledjChisl.conf.lua b/src/.sledjChisl.conf.lua
new file mode 100644
index 0000000..6b8f29b
--- /dev/null
+++ b/src/.sledjChisl.conf.lua
@@ -0,0 +1,58 @@
1-- sledjChislConfig.lua
2
3-- This works coz LuaJIT automatically loads the jit module.
4if type(jit) == 'table' then
5 io.write('.sledjChisl.conf.lua is being run by ' .. jit.version .. ' under ' .. jit.os .. ' on a ' .. jit.arch .. '\n')
6else
7 io.write('.sledjChisl.conf.lua is being run by Lua version ' .. _VERSION .. '\n')
8end
9
10config =
11{
12 ["performance"] = "default"; -- fast, balanced, default, lean
13 ["debug"] = true;
14 ["scRoot"] = "/opt/opensim_SC";
15 ["scUser"] = "opensimsc";
16 ["Tconsole"] = "SledjChisl";
17 ["Tsocket"] = "opensim-tmux.socket";
18 ["Ttab"] = "SC";
19 ["loadAverageInc"] = 0.7;
20 ["simTimeOut"] = 45; -- seconds
21 ["webRoot"] = "/var/www/html";
22 ["webHost"] = "localhost";
23 ["URL"] = "sledjchisl.fcgi";
24 ["webIframers"] = ""; -- Space separated list of hosts allowed to iFrame us, coz someone asked. Include the "https://" bit.
25 ["seshRenew"] = 10 * 60; -- seconds
26 ["idleTimeOut"] = 30 * 60; -- seconds
27 ["seshTimeOut"] = 24 * 60 * 60; -- seconds
28 ["newbieTimeOut"] = 30; -- days
29 ["pepper"] = "My long beard is salt and pepper coloured, though there are no birds in it, only breakfast.";
30 ["ToS"] = [[
31
32Don't do anything that is illegal anywhere in the world.
33
34Well, that wont work, almost everything is illegal somewhere in the
35world.
36
37Don't do anything that is against the moral code of the system admins.
38
39Well, except that one thing, you know, that they'll put up with coz they
40are nice people, but it's wrong m'kay.
41
42Don't be mean to anyone, except Dave, coz he smells evil.
43
44Well, it's not that Dave smells evil, he's just differently fragranced,
45and our overwashed germophobe society is well trained to demonize those
46that smell differently. So be extra nice to Dave, coz he's a great guy,
47and is tired of everyone being mean to him just coz he's trying to be
48good for the environment and his health. Which means he smells different
49to whatever perfume is fashionable this year, coz the corporations want
50to sell that this year. I blame marketing, they're actually evil.
51Sorry, went off on a rant there.
52
53Oh just respect and be nice to everyone dammit, unless they ask nicely
54otherwise. Also be good for the environment and stay healthy.
55
56]];
57}
58return config
diff --git a/src/BuildIt.sh b/src/BuildIt.sh
new file mode 100755
index 0000000..6696210
--- /dev/null
+++ b/src/BuildIt.sh
@@ -0,0 +1,132 @@
1#!/bin/bash
2
3mkdir -p build
4
5# Poor mans git sub modules / subtrees, coz otherwise it gets complex.
6if [ ! -d git-sub-modules/fcgi2 ]; then
7 pushd git-sub-modules >/dev/null
8 git clone https://github.com/FastCGI-Archives/fcgi2.git
9 popd >/dev/null
10else
11 pushd git-sub-modules/fcgi2 >/dev/null
12 echo "Updating fcgi2."
13# git pull | grep "Already up-to-date." && rm -fr build/fcgi2
14 popd >/dev/null
15fi
16
17if [ ! -d build/fcgi2 ]; then
18 cp -r git-sub-modules/fcgi2 build/
19 pushd build/fcgi2 >/dev/null
20# make distclean
21 ./autogen.sh
22 ./configure
23 sed -e "s/#define PACKAGE/#define FCGI_PACKAGE/g" -i fcgi_config.h
24 sed -e "s/#define VERSION /#define FCGI_VERSION /g" -i fcgi_config.h
25 make
26 popd >/dev/null
27fi
28
29echo ""
30echo ""
31
32if [ ! -d git-sub-modules/luajit ]; then
33 pushd git-sub-modules >/dev/null
34 git clone https://luajit.org/git/luajit-2.0.git
35 mv luajit-2.0 luajit
36 pushd luajit >/dev/null
37 git checkout v2.1
38 popd >/dev/null
39 popd >/dev/null
40else
41 pushd git-sub-modules/luajit >/dev/null
42 echo "Updating LuaJIT."
43# git pull | grep "Already up-to-date." && rm -fr build/luajit
44 popd >/dev/null
45fi
46
47if [ ! -d build/luajit ]; then
48 rm -fr build/luajit
49 cp -r git-sub-modules/luajit build/
50
51 pushd build/luajit >/dev/null
52 make clean
53 make amalg
54 popd >/dev/null
55fi
56
57echo ""
58echo ""
59
60if [ ! -d git-sub-modules/qlibc ]; then
61 pushd git-sub-modules >/dev/null
62 git clone https://github.com/wolkykim/qlibc.git
63 popd >/dev/null
64else
65 pushd git-sub-modules/qlibc >/dev/null
66 echo "Updating qlibc."
67# git pull | grep "Already up-to-date." && rm -fr build/qlibc
68 popd >/dev/null
69fi
70
71if [ ! -d build/qlibc ]; then
72 rm -fr build/qlibc
73 cp -r git-sub-modules/qlibc build/
74
75 pushd build/qlibc >/dev/null
76 make clean
77 ./configure
78 make
79 popd >/dev/null
80fi
81
82echo ""
83echo ""
84
85if [ ! -d git-sub-modules/toybox ]; then
86 pushd git-sub-modules >/dev/null
87 git clone https://github.com/landley/toybox.git
88 popd >/dev/null
89else
90 pushd git-sub-modules/toybox >/dev/null
91 echo "Updating toybox."
92# git pull | grep "Already up-to-date." && rm -fr build/toybox
93 popd >/dev/null
94fi
95
96if [ ! -d build/toybox ]; then
97 rm -fr build/toybox
98 cp -r git-sub-modules/toybox build/
99 ln -fs ../../../boxes build/toybox/toys/boxes
100 ln -fs ../../../sledjchisl build/toybox/toys/sledjchisl
101 ln -fs ../toys/sledjchisl/fcgi_SC.c build/toybox/lib
102 ln -fs ../toys/sledjchisl/fcgi_SC.h build/toybox/lib
103 ln -fs ../toys/boxes/handlekeys.c build/toybox/lib
104 ln -fs ../toys/boxes/handlekeys.h build/toybox/lib
105
106 pushd build/toybox >/dev/null
107 sed -e "s/strend(/tb_strend(/g" -i lib/lib.h
108 find ./ -type f -name "*.c" -exec sed -e "s/strend(/tb_strend(/g" -i {} \;
109 make clean
110 #make defconfig
111 #make menuconfig
112 make allnoconfig KCONFIG_ALLCONFIG=../../miniconfig || exit 1
113 popd >/dev/null
114fi
115
116echo ""
117echo ""
118
119pushd build/toybox >/dev/null
120export CFLAGS="-I../luajit/src -I../fcgi2 -I../fcgi2/include -I../qlibc/include/qlibc $(mysql_config --cflags) -g3"
121export 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"
122make || exit 1
123popd >/dev/null
124ln -fs ../src/build/toybox/toybox ../bin/sledjchisl
125
126#sudo rm -f /opt/opensim_SC/var/cache/sledjchisl.socket
127#sudo rm -f /opt/opensim_SC/var/cache/sessions/*
128#sudo rm -f /opt/opensim_SC/var/lib/users/*
129#sudo spawn-fcgi -n -u opensimsc -s /opt/opensim_SC/var/cache/sledjchisl.socket -M 0660 -G www-data -- /usr/bin/valgrind --leak-check=full build/toybox/generated/unstripped/toybox sledjchisl
130##sudo spawn-fcgi -n -u opensimsc -s /opt/opensim_SC/var/cache/sledjchisl.socket -M 0660 -G www-data -- /usr/bin/valgrind --leak-check=full build/toybox/generated/unstripped/toybox sledjchisl 2>&1 | tee log.txt
131##sudo spawn-fcgi -n -u opensimsc -s /opt/opensim_SC/var/cache/sledjchisl.socket -M 0660 -G www-data -- /usr/bin/valgrind --leak-check=full --show-leak-kinds=all build/toybox/generated/unstripped/toybox sledjchisl
132###sudo spawn-fcgi -n -u opensimsc -s /opt/opensim_SC/var/cache/sledjchisl.socket -M 0660 -G www-data -- /usr/bin/ddd build/toybox/generated/unstripped/toybox sledjchisl
diff --git a/src/boxes/BOXES.txt b/src/boxes/BOXES.txt
new file mode 100644
index 0000000..745b7c9
--- /dev/null
+++ b/src/boxes/BOXES.txt
@@ -0,0 +1,1130 @@
1What's needed for MC like program. Call the library toyboxes, and the MC clone toysoldier. B-)
2
3Use ANSI for terminal control.
4UTF-8 support is welcome in toybox where it makes sense, here it makes sense.
5
6Command defining - key, name, script, context.
7 Internal commands for the scripts.
8 Which can depend on the context.
9 Context is important. Gotta handle stuff like vi modes, MC browse / view / edit modes, less search popping into readline mode, etc.
10 Learnable keys ala GIMP.
11
12Split the screen up into boxes.
13 Each box is a context.
14 Current box should have it's box graphics drawn in a different colour to highlight it.
15 Tab/shift-Tab cycles through boxes.
16 Though the editor has it's own use for tab. Ctrl-tab as an alias perhaps?
17 Horizontal / vertical splits, with some sort of size control.
18 Each box can be split h or v once,
19 with a control of what proportion the current box has (initially half),
20 a minimum size set by the contents,
21 and initially a copy of the pointer to the function that supplies it's contents / deals with it's input / includes it's context.
22 Later it can have that pointer set to something else.
23 Any given box can be deleted, which deletes it's sub boxes, drops it's pointer, and merges it's area with the one it split from.
24 See if screen and tmux agree on keys to use for box control.
25 Though, as usual, it's definable, but screen/tmux can be the defaults.
26 Choose to make a box full screen. With menu to select other boxes, or swap back to full set of boxes.
27 Can be without borders and such.
28
29 The borders can be the usual box graphics, +-| characters, or inverse spaces, in that order of priority.
30 Show bits of text on top and bottom borders (left and right sides of them).
31 MC includes a couple of tiny mouse controlled widgets.
32 Perhaps a scroll widget in left or right borders. Emacs dired has that.
33
34 Single line 'boxes', across the entire terminal, or across each box.
35 Should be context sensitive.
36 Menu - a list of commands showing their names and keys, with sub menus.
37 Function keys - Any function keys with commands showing the command names and key.
38 Perhaps could just be a specialized menu.
39 Programmable status line.
40
41 Contents scrolling.
42 Virtual memory buffers.
43 If a file viewer, just mmap it.
44 Editor should be able to only load in bits of a large file, perhaps with a UNDO/REDO buffer.
45 If command output, can create a temp file and mmap it.
46
47 file viewer (less, man).
48 editor (vi, mcedit, nano).
49 Like e3, make generic editor, with pluggable command keys, that kick in depending on how it's called.
50 Uses a file list in a box to select files for opening.
51 Poor mans top and similar commands.
52 Top at least could make use of internal boxes, with the command list using a sortable list.
53 Put command output in boxes. Each line can be selected and operated on.
54 Title bar across top, with click to sort style column names.
55 Each line is a single line 'box'.
56 ls.
57 ls with display options.
58 archive listing
59 find command result.
60 All these can show current directory with diving in and out, or tree and current directory in a sub box.
61 Also, allow an edit mode for editing the file attributes that are displayed, inline and full box if possible.
62 Shell console, with various text substitutions on input.
63 Can be a single line 'box' as well as an ordinary box.
64 Though the ordinary ones show their output in their box,
65 but the single line one should swap to a full screen, which can be a full screen box.
66
67A box has content. Each content type is a context. So we can have -
68 Plain scrollable text view.
69 Fancy text view (hex and so on).
70 Text edit.
71 Directory browse.
72 Script controlled.
73
74Popup widgets centered on box/boxes that they affect.
75 Labels, Text line (with optional history), check boxes, radio buttons, OK/Cancel/etc buttons, popup select list (useful for history to, can be just a menu specilization).
76 Widget sets, though try to keep things simple enough to not need them.
77 Notifications, with the addition of an abort button.
78 If a single widget in the popup, prompt in a single line "box" near the bottom (like nano).
79 Options.
80 Keep options down to a bare minimum.
81 Command params.
82 Fetch command params from internal toybox structures, map them to the appropriate widget.
83 F2 menu - list of commands to apply to file/s or current directory.
84 The list is selectable by file type.
85 Should use the same code as the menu box, and allow sub menus.
86 Context sensitive history lists for selecting stuff.
87 Could also be a widget inside the popup when needed.
88 Search / replace.
89 Delete / save confirmation.
90 Command progress meter.
91 Use xargs, and have it output something useful per 'argument' for the progress meter.
92 xargs has an option to print the command to stdout, and to ask for confirmation per command.
93 xargs can run multiple threads.
94
95Scripting, so we can make things fancy and tie it together.
96 Don't forget to make it 'scriptable' via internal C.
97 MC uses that for the F2 menu, user menu, and archive access.
98 We should use scripts to define most of the above.
99 I'd like to use Lua, but shell is likely a better choice.
100 Coz toybox will have a shell.
101 And MC scripts are mostly shell bits.
102 Should have an actual toyboxes command that you can feed toyboxes scripts into.
103 Might actually get away with using that to define most of MC, AND be the editor plugins.
104 See how far I get, but that's what I'll start with for testing.
105 I should reuse the old emu protocol for this.
106 Not gonna actually sort function names or key combos.
107 A linear search is good enough for keys, they only come in at human speeds.
108 People might want the scripts to run faster, but in toybox we strive for simplicity.
109 Well, maybe a binary search within each modules function block that is sorted by hand in the source code.
110 On the other hand, might be able to make use of the toybox command parsing infrastructure for script functions.
111 Leaving that as a problem for toybox itself.
112 But it's not reusable, it uses globals, and not sure if we can screw with those globals.
113
114NOTE - toybox is designed to only deal with one command per process. So we can't call other toybox commands internally.
115 Or can we? Toysh does it.
116
117Events
118 We need a time event. Top has a resolution of hundredths of a second, though says that only tenths is officially supported.
119 Tenths makes sense for human speed UI. Hundredths makes sense if you want video frame rates. lol
120 Termios only allows tenths of seconds for read anyway.
121 Hmm, seems read() wants to wait for at least one byte, it's a between byte counter. Doh!
122 Select() is likely less simple, poll() the same, and epoll() seems to be linux specific. All allow more precise timeouts.
123 On the other hand, Rob is using poll() in netcat.
124
125Common bits / differences.
126 Initial toybox command arguments.
127 Ability to change those within the editor.
128 Files - passed as arguments, or can add / remove them from the running editor.
129 Process the file through some proggy, or just some function of the editor script.
130 Save / save as / backups.
131 Modelines are actualy discouraged as a security issue by the standard, but encouraged by toybox to use vi modelines.
132 Filename completion.
133 Filename prompts could have a couple of features.
134 Can pass things in and out of a shell command, append to the file, edit a specific fixed section of a file or special file (disk block editing!), or use stdin and stdout (joe in a pipe).
135 Directory / file browsing in a window.
136 Windows per current design.
137 Multiple files in the command line each have their own window.
138 Different / same file in each.
139 Each has it's own cursor / marks / block, etc.
140 Delete / scroll some other window.
141 Open a file in some other window, possibly creating one first.
142 Show one window full screen.
143 Method to show some hidden window, can be hidden if there's not enough space for them all.
144 Buffers - holds the contents of the files being edited / viewed.
145 Attached to windows, but can shift them around?
146 Edit / view / read only, named / unnamed buffers.
147 Special purpose buffers.
148 Emacs has - scratch, help, grep, compile, gdb, man, shell, probably others.
149 Though most of those are just running some other command in a window.
150 Kill ring buffer.
151 Kill goes to the buffer, delete just vanishes, on the other hand, this is really the difference between "cut" and "delete".
152 "Yank" just means "paste from kill buffer" then.
153 Emacs can have different working directory for each buffer, OR one global directory.
154 List them, perform some operation on the members of the list. Go through them all, prompting to save modified buffers.
155 Display text - navigate within it, scroll it in various ways.
156 Many ways to display otherwise unprintable text.
157 Inverted video for high bit characters.
158 Just show high bit characters.
159 UTF8.
160 ^X
161 Could be a problem with "where's my cursor" for the code.
162 Hex mode, wrap mode, raw / parsed mode, as well as formatted / unformatted mode.
163 Parsed mode has the text being processed by some command specified in the config file.
164 Formatted mode converts common formatting stuff to bold / underline.
165 Line numbers.
166 Scrolling can be definable amounts.
167 Some editors count buffer lines, some display lines.
168 Move to top, bottom, middle of screen / line.
169 Marks.
170 One mark and cursor.
171 Hmmm, Emacs uses the idea of a "point" which is between characters, with the "cursor" on the right side.
172 Not sure if this will be a problem.
173 Emacs has only one mark, and everything between point and mark is the "region", a block I think.
174 Multiple marks - numbered, named, just arbitrary, automated marks for various reasons. Line / line and character marks.
175 Next / previous / goto / remove one / all marks.
176 Whitespace / word boundaries / line / paragraph / "sections", etc. Should be definable.
177 Vi has multiples types of all of them. Pffft
178 Smooth scrolling (line by line).
179 Slow serial line support - do we need it?
180 Maybe. B-(
181 Status line.
182 Show cursor position, details of file / character under cursor / working directory. Often used for line input of parameters to.
183 Top of screen, bottom, above the key display in nano. Can have left, middle, right widgets.
184 Nano has essentially two status lines.
185 Expert mode to turn it off, disable it for more screen space.
186 Regexs - basic / extra / extended
187 Replacable stuff in search & replace. Ex/vi has this as an option.
188 Commands - invoked immediately with no echo, or typed via readline.
189 Pre command numbers (usually not echoed) / post command arguments. Also pre command regexs and other things to select the lines to work on.
190 Methods of repeating commands. Repeat last / next command, possibly X times.
191 Direction / motion.
192 Execute line / buffer / file.
193 Select lines, apply command to them.
194 Parameter expansion.
195 Key handling.
196 Bind / learn keys.
197 Emacs has "keymaps" for major and minor modes, as well as a global one.
198 Unbind keys. Mask keys from a lower level, but without actually binding them to anything, a NOP.
199 Command keys can be one or more keys.
200 Emacs and wordstar generally use a control key followed by some other key.
201 Show help page in a window with key bindings. Show binding for specific key.
202 Shortcut keys display. Nano has two lines of 6 each, showing only the most common. MC has one line, showing 10 function keys. No one else cares?
203 Esc key same as Alt / Meta key OR used for function keys OR used by itself. sigh
204 Meta key used to insert high bit characters.
205 Abort current command key.
206 Emacs has a keymap per buffer, which is the keybindings.
207 A global keymap.
208 The buffers major mode keymap.
209 Minor modes can have keymaps that override the major mode keymap when the minor mode is turned on.
210 Del <-> BS swapping.
211 Readline type widget. Called "minibuffer" in emacs.
212 For command parameters, also just go to one at the bottom to type commands into.
213 A fullscreen editor could be considered to just be a stack of these.
214 History, completion, editing, escape from. Position and length.
215 Same editing keys, and treat it just like a one line window.
216 Restricted or different editing keys while in some random line editing mode.
217 Moving readline to some other line (the basis of full screen editing perhaps).
218 Perhaps allow it to autoexpand if the input covers multiple lines.
219 Though that might be better to do as just creating more lines, then moving the readline between them.
220 Still would need the prompt on the top one, and to know to send them all at once when done.
221 How exactly does one create multiple lines?
222 Only way that makes sense is if the ENTER key is different from the "now do this" key.
223 Or wrapping long lines instead of scrolling them.
224 The prompt could include the default in ().
225 Mouse support - left click, double click, scroll wheel.
226 Only used to click on a widget, move cursor, or scroll around.
227 Shift click is used for X cut and paste support, think that just works from the terminal.
228 Shell - In a window / full screen.
229 Make editor a background task with bg / fg standard shell stuff.
230 Pass a block or buffer as stdin. Append / insert stdout to the buffer.
231 The shell output can just appended to the window contents as text that can be edited like every other window. When the cursor is at the bottom of the shell windov, stuff typed in is the next shell command.
232 Simple editing.
233 Move cursor, insert, delete, overwrite.
234 Basic editing.
235 Modes.
236 Vi has a lot of them - command, insert, ex, and moooore.
237 Emacs has definable modes that are a different concept to vi.
238 There are "major" and "minor" modes per buffer.
239 Major modes is for file type specific editing like "editing C, latex, etc" or "Dired".
240 Includes things like indenting, syntax highlighting, function boundaries (for ctags I guess), invoking the right compiler, keymaps, etc.
241 Can include other specialisations, like a python shell, and dired.
242 Minor modes are extras you can add, they seem to be things like "autofill", "wrap", "overwrite", "line numbers", etc.
243 Insert / overwrite mode.
244 Automatic detection of file type (typically source files) to put editor into different modes.
245 Cut, copy, paste, delete - blocks, word / line /etc, to end / beginning line, others.
246 To / from other buffers, files, "clipfile", maybe even the clipboard/s (thought that's an X thing I think).
247 Blocks - cut, copy, move, write to file.
248 Unhighlight block.
249 Search / replace - regex / shell glob / straight text / whole words. Case / charset sensitive. Forward / backward.
250 Regexs - basic / extra / extended
251 File / all files / within selection. Wrap around searching.
252 Incremental. Interactive / all replace. Inverted (find non matches).
253 Highlight / filter found.
254 Regex for the replace bit. Replacable stuff in search & replace. Ex/vi has this as an option.
255 Can search mixed hex and strings.
256 History, repeat, repeat in other direction.
257 Modified state.
258 Can be used by some commands to pester the user.
259 Ability to turn that state off.
260 Tabs / spaces / half tabs. Setting tab size.
261 Advanced editing.
262 Insert a character that is otherwise hard to insert, like command keys.
263 Insert date/time.
264 Quick search for a character in current line / forward / back.
265 "Smart" home, home goes to actual beginning, or first non blank.
266 Replace lots of space horizontally / vertically with just one / zero spaces.
267 Insert empty line/s above / below.
268 End of line space removal.
269 Allow cursor beyond end of line.
270 Visible white space.
271 DOS / Mac line ending convert. On the other hand, might just be good to do that transparently, remember on read, convert on save.
272 Change text encoding.
273 Add a newline at end of file if it's missing one.
274 Think we are doing that anyway, but an option to turn that off might be good.
275 Transpose, upper, lower, reverse case, capitalize words.
276 Adding a prefix / suffix string to selected lines.
277 Indent / outdent / line wrapping / centreing which can be auto / manual. Left and right margins.
278 Column blocks.
279 Deleting might just replace the column block with spaces / tabs.
280 Macros. Various ways of defining / invoking a macro.
281 Nested macros.
282 Auto expanding abbreviations.
283 Auto save after certain commands, or certain time.
284 Multi level undo / redo. Also undo current line (revert it?).
285 Disable undo.
286 Ability to list the undo "records"?
287 Emacs undo boundaries?
288 Spell checker.
289 Pretty printer (likely via shell command like indent).
290 Email quote handling.
291 Count / highlight lines matching or not matching regexes.
292 Sort block.
293 Persistant cursor position and selection.
294 Complete the word being typed based on other words in the file.
295 Code editing.
296 Ctags - basically lookup a symbol (word cursor is on) in the ctags files, which gives you a position in some other file that defines this symbol. Then display this other file somehow, probably allowing editing.
297 Ctags is in the standards, but do we want to write a toybox version?
298 Bracket / character matching. Goto / highlight matching bracket. Auto insert the closing one.
299 Include "insert one, blink the other".
300 Auto detect code block for indenting?
301 Syntax highlighting.
302 Next / previous error (compile errors usually).
303
304Readline.
305 toybox has get_line() and get_rawline(), but they are not interactive readlines.
306 In order to have our readline() be useful for generic use, the keystrokes that do stuff must be definable.
307 So, what do we need?
308 GNU readline has (leaving out a few things) -
309 Editing
310 How is a "word" defined?.
311 Hitting ENTER anywhere in the line submits it. An editor would want to actually insert the ENTER into the text.
312 Move cursor / insert / delete / backspace / undo (all the way back) / revert.
313 Perhaps redo might be nice.
314 Move cursor to start / end of line, forward / back one word, redraw.
315 Cut / copy / paste. Kill ring with ring rotation?
316 Entire line.
317 All spaces around cursor.
318 From cursor to mark.
319 Highlighted block.
320 From cursor to start / end of line.
321 to end of word, or end of next word if between words.
322 to beginning of word, or of the previous word if between words.
323 to previous white space. Different from above coz apparently white space is different from word boundaries. shrugs
324 Numbers in front of commands. Vi and emacs probably need this? More and less "needs" this. Otherwise... eww.
325 For repeats, or sometimes to reverse the direction if it's negative.
326 Insert / overwrite mode.
327 Upper and lower casing characters or words.
328 Transpose characters / words.
329 Marks - setting a mark and moving the cursor to it / swapping with it.
330 Character search within the string - forward / back.
331 Insert comment - bloat we don't need.
332 History.
333 Back / forward one line. Goto start / end of history.
334 Incremental and non incremental searching, forwards or back.
335 During incremental search - ability to abort and restore original line.
336 Remember the last (incremental?) search.
337 Either return the found history (ENTER), or allow editing (editing keys).
338 Option to save edited history lines, and mark them as edited.
339 Show key bindings / macros / etc.
340 Tab completion.
341 List / select completions.
342 Cycle through the matches.
343 Macros!
344 Config files.
345 Not likely to emulate these, we have our own needs, and it's not a standard.
346 Expansions.
347 MC will want argument expansions at least.
348 Though perhaps this is best left to the code that calls this to expand the result.
349
350more
351 Can handle switch between multiple files.
352 Searches are regexs. Can also search for non matches.
353 Multiple marks.
354 Forward and back half screenful, with "half" being the specified number, but defaulting to actual half.
355 Shell commands (not in the standard).
356 Invoke the editor mentioned in EDITOR, or default to vi. Pass line number if it's vi.
357 Print info about file.
358 Ctags.
359 Some commands are single letters, some are ":" followed by a single letter, possible with more text, then ENTER.
360 Or other variations.
361
362less
363 Has bracket matching, but only from top-open / bottom-close.
364 Can search between multiple files, or add more.
365 Files on the command line, add / remove files, just open a new file now.
366 Highlight found text.
367 Filter found lines.
368 Option to NOT do regex search.
369 Change the command line arguments while running.
370 Shell commands with replacable params.
371 Input processor - some proggy that the input file is processed through, the output of that is shown.
372 Um, why not use pipes and input redirection?
373
374ed - obsolete for toybox, ancestor of ex.
375 A line editor.
376 In command mode, type the command, then ENTER.
377 In input mode, type text, with "." on a line by itself to go back to command mode.
378 Uses one or two optional line addresses (can be regex) followed by a single character command, then arguments.
379 The "address" can be various symbols with various meanings, see the manual.
380 Usual basic editing and navigation commands.
381 Join lines.
382 Mark lines.
383 Display lines (with line numbers).
384 Copy / move lines.
385 Apply a command to a bunch of lines matching / not matching the address.
386 Insert a file / write lines to a file.
387 Search and replace.
388 Undo.
389 Shell command, with filename replacement character.
390
391sed
392 Stream editor.
393 Fairly similar to ed, except it applies a script of commands to each line in turn.
394 Branch to a label within the script. Can test if a substitution happened before deciding to branch.
395 Includes a "hold space" for cut and paste type operations, as well as swapping.
396 Can output line numbers.
397 Can read in a file, or write to one.
398 Can be commented.
399
400ex - obsolete for toybox, but part of vi.
401 A line editor, apparently the line oriented editing mode for vi.
402 So while the command itself is obsolete, it's internal stuff might be needed for vi.
403 In fact a lot of the standard for vi just refers to ex.
404 ":" means to perform an ex command from vi. lol
405 Starts in command mode (":" prompt).
406 Basically a "type command then ENTER" command mode.
407 Text input mode (append, insert, change) ended by "." on it's own line.
408 Has line addresses before the commands, similar to ed.
409 Commands have complex parsing requirements. Ewww.
410 Has some really basic command line editing.
411 Has a bunch of buffers, with modes. Commands apply to a named buffer, or the unnamed one if no name is given.
412 Abbreviations and maps. Same thing? They interact. lol
413 Think the difference is that maps don't have to have blank space after them.
414 They expand during typing.
415 Seems that maps are for binding to keys?
416 Set various options.
417 Shell command, with optional interaction.
418 Read commands from a file.
419 Can scan ctags files looking for regexs.
420 Can switch to "open" and "visual" modes, whatever they are.
421 "Visual" mode is just vi.
422 "Open" mode I think is a vi for dumb terminals?
423 Has a concept of "window", also not sure what that is. Might just be the number of terminal lines.
424 Shell escape - pass selected lines to a shell command, replace them with whatever it returns.
425 Shift lines back and forth. Indent and outdent in other words.
426 Execute a buffer as ex commands.
427 Regexs have extra thingies.
428 Replace commands can refer to other bits of text using parameters. See the manual.
429 Autowrite - basicaly save the file after certain commands.
430 Mode that strips out non printables on file read.
431 Can be made ed compatible. lol
432 Can display line numbers.
433 Paragraph boundary pairs can be set.
434 Definable scroll distance.
435 Visual and open modes have "sections", with definable pairs of boundary characters.
436 Can show matching braces.
437 Can optionally warn on some commands if the buffers are modified.
438 Margin auto wrap and end of line blanks removal.
439 Can wrap searches.
440 "modelines" (as used at the top of toybox source files) are apparently is strongly discouraged by the standard. shrugs
441 Overlaping copies are allowed.
442 Automatic marks created sometimes.
443
444vi
445 Notable for it's variety of modes, and it's standard command mode that uses non echoed ordinary keys for common commands.
446 "Visual" editor, a screen oriented superset of ex, that lets you use ex commands.
447 A lot of the standard simply refers to ex.
448 Seems to mostly be similar to ex commands, only done full screen, and with full screen navigation.
449 Has "open" and "visual" modes, but not sure what they are. Also "ex" mode, with means using ex commands.
450 I think "open" mode is for dumb terminals, it's all done on the bottom line, perhaps with full screen redraws.
451 "Visual" mode then must be full screen editing.
452 Text input mode can return to command mode with Esc.
453 Has five kinds of "words" and four kinds of "bigwords", six types of "sections", four types of "paragraphs", and three types of "sentences".
454 There can be a multi digit count before commands, it's not echoed anywhere when not in a command line mode, the command key then does its command count times.
455 For instance typing "12h" does not echo anything, but just moves the cursor back 12 places when the "h" is hit.
456 Lots of those seem to be letters, or control keys, though there are others.
457 [count] ! motion shell-commands <newline> run a shell command, replacing count lines with the output. Looks like you get to type and edit the command while you see it.
458 Move to matching brace.
459 Repeat last command, for specific commands.
460 Move to last context, where I think "context" means where we was before the last command, it gets a special mark.
461 Move to words, bigwords, sections, paragraphs, etc.
462 Reverse case.
463 Find character in current line. Move cursor to before / after specific character.
464 Move to top / middle / bottom of screen.
465 Insert empty line above / below.
466 Paste above / below, depending on if the buffer is line or character mode as well.
467 Replace count characters with the entered character.
468 Undo current line.
469 Some editing command keys while in text input mode.
470
471nano
472 Has a file browser mode.
473 Has a title bar, with three areas for showing status type info.
474 And a status line, which shows messages and lets users type stuff like file names.
475 Optional shortcut lists show 12 of the most common keystrokes in the current mode, and can be mouse clickable.
476 Mouse moves the cursor, double click sets marks.
477 "Smart" home - the usual go to first non blank, or go to actual start of line.
478 Tabs to spaces.
479 Multiple file buffers.
480 Search and replace history.
481 Deal with DOS / Mac line endings sanely.
482 Restricted mode.
483 Smooth scrolling.
484 Setting tab size.
485 Syntax highlighting.
486 Display cursor position.
487 Backspace and delete fixes, coz that's always confusing.
488 Autoindent.
489 Optional line wrap.
490 "Soft wrapping", no idea what that is.
491 External spell checker.
492 Undo and redo.
493 Hard to use method to insert otherwise unusable characters.
494 Optional search case sensitivity.
495 Optional regex searches.
496 Email quote handling.
497 Source code bracket handling of some sort.
498 Turning command line options on and off within the editor.
499 Write selected text to a file.
500
501microemacs (microGNUemacs lol)
502 Go to line number.
503 Scratch buffer, help buffer, grep buffer, compile buffer.
504 Named buffers - per file. Can have their own working directory, or a global one for all.
505 Kill a named buffer, prompting if it's changed.
506 List buffers.
507 Has the concept of "modes", default ones are - fill (wrap), indent, overwrite, and notab.
508 Can set a list of (minor?) modes as default for buffer creation.
509 Run a script (commands from a file).
510 Split windows. B-)
511 Window / buffer specific cursor and mark (depending on which emacs you have).
512 Can swap them.
513 Esc key same as Alt-key.
514 Get help, describe binding, describe key briefly (hit a key, it's binding is displayed).
515 Apropros - prompt the user for a string, open help buffer, list all commands with that string.
516 Auto execute - a shell glob pattern that is matched on files read into buffers, then executes a command.
517 Recenter - position cursor in center of screen / center of line / center of line counted from the bottom.
518 Open line - open up some space by inserting empty lines below cursor, leaving cursor at the top of them.
519 Quoted insert - insert the next key verbatim, ignoring what it's bound to.
520 Universal argument - repeat the next command 4 times, can apply to itself.
521 Toggle read only.
522 Find file read only.
523 Find alternate file - loading a different file into the current buffer, erasing the original buffer contents.
524 Find file in buffer, or load it into new buffer, then switch to it.
525 Find file other window - opens file in new buffer, splitting the window if needed.
526 Delete window.
527 Delete other windows.
528 Split window.
529 Enlarge / shrink window.
530 Next / previous window.
531 Scroll other window - scrolls the next window X pages.
532 Switch to buffer other window - "switch to buffer in another window"?
533 Switch to buffer - ask which buffer should be in this window.
534 Switch to a shell screen, and back again.
535 What cursor position - show a bunch of info about cursor position and what's there.
536 Next / previous error.
537 Dired - basically a multi window MC. lol
538 Save some buffers - looks for buffers that need saving and prompts user.
539 Just one space - delete all white space around cursor, then insert one single space.
540 Query replace - an interactive search and replace. Also a replace all with no interaction.
541 Oh nice - can do a search and replace with a regex for both the search string, AND for the replacable strings (selecting which ones get replaced with that regex).
542 Beginning / end of buffer.
543 Capitalize / upper / lower word.
544 Delete word.
545 Delete blank lines.
546 Delete horizontal / vertical space.
547 Delete leading / trailing space.
548 Delete lines non / matching regex after cursor.
549 Hmm, difference between "kill" and "delete"?
550 "Kill" goes into the kill buffer, "delete" just vanishes.
551 "Yank" inserts things from the kill buffer.
552 Kill buffer is a ring buffer in full emacs, but might not be a ring in microemacs.
553 So it's just an old fashioned concept for "cut and paste".
554 Fill paragraph - just means to wrap and justify it. Command to ask the user what the wrap column is.
555 Copy region as kill.
556 Execute extended command - does the readline thing to ask user for a command and arguments to run.
557 Execute one buffer / line.
558 Not modified - turn off the modified flag in current buffer.
559 Blink and insert - insert a character, then search backwards for it's match (bracket, or the character itself) and blink that.
560 Mini buffer - it's just a readline for command arguments and such, but treated as a one line window, with an uneditable prompt.
561 It does have the trick of expanding to more lines if it's content goes over multiple lines.
562 The prompt can include a default argument in ().
563 Numbers can proceed commands, seems to be what the universal argument is for.
564 Looks like they have a key to introduce them, then get put into the mini buffer.
565 "Digit-argument" and "negative-argument" might be the functions for that.
566 Swap Del and BS.
567 C program "mode" - no idea what that involves.
568 Count regex non / matches.
569 Define key (includes a keymap, what ever they are). Can also undefine a key, and there's a "global" keymap. Can also bind keys in specific modes.
570 Meta key can insert 8 bit characters, or not.
571 Keyboard quit - abort current action.
572 Insert spaces instead of tabs.
573 Toggle overwrite / insert mode.
574 Add a prefix string to lines in a selected region. Or set that string.
575 Show working directory in the status line.
576 Refresh screen, including recomputing window sizes if needed.
577 Scroll window without changing cursor position within window.
578 Toggle read only.
579 Undo boundaries?
580 Undo on/off.
581 List undo records for current buffer, in a new buffer.
582
583wordstar (joe, turbo C) - Using joe, which also comes in emacs and pico flavours.
584 Help window, with commands to page through that help text.
585 Something about "inverting" 8 bit characters, but we want to be 8 bit clean. Not a WordStar thing I think. shrugs
586 Ah, for displaying high bit characters in other languages, normally inverts the display, but "as is" mode just prints them normally.
587 Option to auto append a new line at end of files on save.
588 Option to disable the status line.
589 Windowed & buffered like emacs, but for when it's pretending to be emacs.
590 Can be windowed anyway.
591 Split window on same file.
592 Make one window full screen.
593 Can move to hidden windows (hidden when there's not enough space for them all).
594 Horizontal split only.
595 Height change.
596 Multiple files on the command line go to multiple windows.
597 Option to define how many lines are kept on screen between page up / down commands.
598 Option to not use X lines at the top, for embedding in a BBS.
599 Multi level undo and redo.
600 Scrolling.
601 Automatic detection of source code files, other types get word wraping and autoindent by default.
602 Can set left and right margins for word wrapping.
603 Can center between them to.
604 Can manually indent lines, highlighted blocks, or autodetected code blocks.
605 Command to insert a single space while in overwrite mode.
606 Overwrite mode makes backspace just move left, no deleting.
607 File name completion.
608 History. History is described as a single line read only edit window, just like other windows, so the usual commands will work in them.
609 Status line can be edited in the setup string, to add or remove escape sequences that include various things.
610 Also a command to show the cursor position and character code in the status line.
611 Suspend the proggy and go to shell. Actually, I think most editors have this in some form or another.
612 Most seem to use the shell foreground/background thingy for this.
613 Search prompts for the thing to search, then prompts for a bunch of character flags that are search options.
614 One of the options is if this is a replace command, then it prompts for the replacement text.
615 It's regexs are slash escaped.
616 Highlighted block can be sent through a shell filter command.
617 Highlighted block can be un highlighted.
618 Macros keyed by digit, and can be nested.
619 Repeat command, hit the repeat, type the numbers, hit the command you want repeated.
620 Also works with characters.
621 Column select mode.
622 Delete block command replaces the entire column with spaces and tabs instead of deleting.
623 Ctags support, prompt for symbol (default is word the cursor is on), search ctags files, replace the file in the current window with the file ctags points to, and the cursor position of the symbol definition.
624 Shell in a window.
625 Very intersening, the shell output is just appended to the window contents as text that can be edited like every other window. When the cursor is at the bottom of the shell windov, stuff typed in is the next shell command.
626 Filename prompts have a couple of features. Can pass things in and out of a shell command, append to the file, edit a specific fixed section of a file or special file (disk block editing!), or use stdin and stdout (joe in a pipe).
627 Keys can be bound in the settings file.
628
629mc viewer
630 Hex mode.
631 Can search mixed hex and strings.
632 Wrap mode.
633 Raw / parsed mode toggle, as well as formatted / unformatted mode toggle.
634 Parsed mode has the text being processed by some command specified in the config file.
635 Formatted mode converts common formatting stuff to bold / underline.
636
637mcedit / cool edit
638 User menu - run a script, insert result.
639 Mark columns.
640 Bookmarks, toggle, next, previous, "flush" (meaning to remove all bookmarks.)
641 Copy / cut insert clipfile as well as prompting for a file.
642 Highlight all lines with found text.
643 Search for hexidecimal, within selection, for whole words, and in "all charsets".
644 Goto matching bracket.
645 Show line numbers.
646 Find "declaration", back from and forward to declaration.
647 It's ctags support.
648 Change text encoding.
649 Create, invoke, and delete macros (hotkeys).
650 Spell check.
651 Email current file.
652 Sort selected lines.
653 Insert output of shell command.
654 Format (wrap) paragraph.
655 Run external format script on selection.
656 Insert date/time.
657 Half tabs.
658 Persistant cursor position and selection.
659 Visible white space.
660 Optional go beyond end of line.
661 Learn keys.
662 Syntax highlighting.
663 Edit syntax and menu files.
664
665Make the fancy features optional in the "wrap if(constant) around things so the compiler can optimise it out" way that Rob likes.
666 So the basic compile choices are -
667 File / stdout viewer.
668 Editor (the following options might depend on how the "pluggable" part is done).
669 Minimal vi?
670 Nice to have vi?
671 Minimal emacs?
672 Nice to have emacs?
673 Nano?
674 Minimal mcedit/cooledit?
675 Nice to have mcedit/cooledit?
676 Lists.
677 Simple top like.
678 Fancy top like.
679 Simple shell.
680 Fancy shell.
681 Basic MC.
682 Full on MC with all bells and whistles.
683 With the fancy compile options being (only if they are substantial amounts of code to support) -
684 Full popups or mere single line prompts?
685 One pane, or multiple panes?
686 Function key single line 'box'?
687 Menu?
688 Status line?
689 Border texts?
690 Border widgets?
691 Sortable lists?
692 History lists?
693 Progress bar?
694 Shell text substitutions?
695 Scriptable?
696 Learnable keys?
697
698Probably got most of what we need to do screen / tmux / multitail, and other full screen terminal stuffs.
699
700Are we ncurses yet? Or twin? lol
701
702------------------------------------------------------------------------------------------------------
703
704NOTES
705
706From Rob -
707----------
708
709On 06/21/2012 09:28 AM, David Seikel wrote:
710> What are the chances that get_optflags() could be made re usable by
711> toys?
712
713Well, right now you can put a NULL as your optstring and then set
714which->options yourself and call get_optflags().
715
716> For toys that need to make their own little scripting system for
717> example. Currently it seems to want to use the toys global, and I'm
718> not sure if it's safe to screw with that after pulling all of my toys
719> options out of it.
720>
721> Would it get reused like that for shell internal commands?
722
723Already does. In toys/toysh.c function run_pipeline() look for the
724TOYFLAG_NOFORK bit.
725
726This probably needs to be genericized somewhere in lib, but I never got
727around to it.
728
729From Rob -
730----------
731
732I need to write a getline() with cursor control, which means I need to
733query the tty size. The magic for that is:
734
735struct winsize tty = { 0, 0, 0, 0 };
736int ret = ioctl(1, TIOCGWINSZ, &tty);
737
738plus $COLUMNS and $LINES, plus echo -e "\e[s\e[999C\e[999B\e[6n\e[u"
739
740Add command history parsing to getline().
741
742From Rob -
743----------
744
745Digging up ancient issues from toybox development, one of which is an
746interesting design issue with querying the terminal size.
747
748When you're on a serial console (happens a lot in the embedded world),
749the local tty device doesn't know the width and height of the window at
750the far end, so ioctl(TIOCGWINSZ) can't report it to you.
751
752If the term program at the other end supports ansi escape sequences
753(everything does, including xterms), there's an escape sequence you can
754use to ask it, but there's multiple levels of non-obvious to the
755implementation.
756
757The escape sequence itself is "\e[6n", to which the terminal program
758responds with another escape sequence, "\e[YY;XXR" where the "XX" part
759is a decimal number with the current cursor's Y location, and XX is the
760cursor X location. (Both decimal, top left is 1;1.)
761
762Since what we want is the size of the screen, we wrap that in some more
763commands, saving the current position, moving to the cursor 999 down and
764999 to the right (which should stick it at the lower right corner),
765query that position, and the jump back to the saved location. The full
766escape sequence is therefore "\e[s\e[999C\e[999B\e[6n\e[u".
767
768The problem is, the response comes back from the terminal program on
769stdin: along with whatever else is coming in on stdin. There could be a
770delay of a significant fraction of a second (especially through a rial
771port, even when you aren't overcommitted and swapping), and there's no
772guarantee the terminal will actually respond. So blocking and waiting
773for a response isn't the greatest idea, and can eat other input the user
774has queued up.
775
776This means you need to do a nonblocking read, assemble an ansi sequence
777a piece at a time (luckily the term programs generate these atomically
778so you don't get part of an ansi sequence with user-typed keys in the
779middle of it), and keep any other data you get for use later. The
780logical place to do this is in the line editing code the shell has to
781have anyway, which responds to cursor keys, page up and down, and so on.
782
783From Rob -
784----------
785
786Less has to get it _right_, as does line editing for a command shell.
787Even before you get to cursor up: unix tty handling is epically crappy.
788My commodore 64 could backspace past the left edge of the screen and
789continue from the right edge one line up. A TRS-80 could do this. Unix
790derivatives _can't_, you have to know when you're at the left edge of
791the screen and do the ansi sequences for "cursor up one, jump right
792999". Which means you have to know when backspace puts you at the left
793edge of the screen, which means you need to know when outputting normal
794characters put you off the _right_ edge of the screen...
795
796That's right: if you don't know what your current screen size is, your
797command shell can't backspace past a line wrap. Welcome to unix
798terminal handling.
799
800From Rob -
801----------
802
803Here's a fun one:
804
805cat /proc/mounts | less
806
807When less outputs the escape sequence to query terminal parameters, it
808has to peek not stdin but /dev/tty. Except the cursor keys scrolling up
809and down also come from /dev/tty, as do "user hit forward slash and
810wants to type in a search regex"... Innit fun?
811
812Plus you have to figure out the width of what you're outputting, which
813means isprint() and probably UTF8 awareness... (Flashbacks to
814fontmetrics in java 1.1, but we can assume monospace text grid...)
815
816From Rob -
817----------
818
819Alas, POSIX does not seem to like simple. To start with, the spec
820requires "more" to be terminal aware, which isn't entirely surprising
821given what it does. But it's not just _height_ aware, it's width aware
822too.
823
824It's actually kinda fiddly: more needs to be able to wrap lines at the
825screen size to figure out when to prompt, but the first 32 ascii
826characters (all the stuff below space) don't consistently print one
827character. Some print nothing, some move the cursor around (tab,
828newline, linefeed, backspace, form feed, whatever the heck "vertical
829tab" does...)
830
831In theory I can hijack catv and just escape most of the low-ascii
832weirdness (so it's two characters, but it's _consistently_ two
833characters whatever $TERM thinks). In practice, when not hooked up to a
834tty more is supposed to act like cat and pass data through unmodified...
835
836I plan to do the simplest standards conformant implementation I can, and
837every once in a while I go "you know, the standard's nuts, let's just
838document the divergence"...
839
840
841From Rob -
842----------
843
844Keep in mind that Unix doesn't implement backspace sanely (like the
845commodore 64 did), thus you have to figure out when you're at the left
846edge of the screen (keeping track of your cursor position)
847
848That's why if you do "echo -n this will screw bash up" and then cursor
849up a few times bash's command history gets all wonky because it _thinks_
850it knows where the cursor is but actually started farther to the right.
851(I contributed ansi escape screen position querying code to busybox to
852improve the situation for ash.)
853
854From Rob -
855----------
856
857The question here is how much of this we already need to do for shell
858history, which is what I was going to implement first. (Actually the one
859I've already sat down and wrestled with is "more", which turns out to
860have rather a lot of these issues as well. Knowing what printable
861characters actually _print_ so you know where your darn cursor is and
862where the screen wraps. You'd think less would have more issues here,
863but more actually hits just about all of 'em...)
864
865Moving the cursor isn't the issue. Querying the screen size is a
866reasonably contained issue. Knowing how the cursor will move when you
867print out an arbitrary string: that's the hard part. (utf8 awareness is
868kinda required here. And I'm assuming a monospaced font even in klingon.
869And replacing the !isprint() characters below space with escape
870sequences...)
871
872
873From Rob and others -
874---------------------
875
876http://lists.busybox.net/pipermail/busybox/2008-October/067348.html
877
878http://lists.busybox.net/pipermail/busybox/2009-May/069314.html
879
880From Rob -
881----------
882
883Implementing ascii programming in the library itself was what I was
884referring to. (It's on my todo list...) All sorts of stuff needs it:
885more, toysh, vi... Even ls wants to know the width of the screen for -C
886mode.
887
888The problem is parsing the replies, since the user could type arbitrary
889stuff. It's stdout that needs to be a tty (because "ls | blah" is not
890going to a tty even if stdin is a tty), but the input _could_ come back
891in on stdin if that's another filehandle to the same tty... as could any
892other random input. If you're filtering all your input through a line
893reading function that needs to parse cursor keys to implement command
894history, doing this is easy. But if _all_ you care about is the probe
895response and you want to leave the rest of the input alone, it's kinda
896hard.
897
898What I might wind up doing is adding it to toysh and having that export
899COLUMNS and LINES environment variables. It wouldn't catch resizes in
900the middle of a command, but I think I'm ok with that...
901
902
903------------------------------------------------------------------------------------------------------
904
905/*
906key / mouse -> command
907menu choice -> command
908border click -> command
909popup + args -> command
910typed string -> command
911script call -> command
912script callback -> command
913C call -> command
914
915Arguments - should use the same argument defining stuff as used by the toys.
916 Keys and menus have to have fixed arguments, and take the rest from their content.
917 Popup and typed get their arguments from what the user selected in the popup, or typed.
918 Popup is told what it's widgets are and how they map to arguments.
919 Popups eventually construct a command string.
920 Script call uses generic text to call the command and set it's arguments.
921 Script callback should be similar.
922 C calls can call the functions direct, or even just pass a string command.
923
924So every command call involves it's content, and can take default arguments from that content, to be overridden by arguments.
925Keys and menus just have arguments coded into their string on definition, though might mostly be argument less.
926Popups have a structure that defines their argument widgets, and how to turn them into command arguments.
927Popups need to be created from string commands to.
928Typed commands can just have their arguments as part of the typed string.
929Scripts and callbacks just send a string that is treated the same way as typed commands.
930C can do the same as scripts, but should be able to call things directly.
931
932Sending commands back to scripts, should use the same format as our commands.
933
934Would be nice to have parameter substitution to, but that would have to be per content.
935
936Use an event system.
937
938---------------------------------------------
939
940Events
941 keystroke / mouse click
942 menu item select
943 timer
944 draw all
945 scroll contents
946 box was redrawn?
947 box destroyed
948 leave box
949 enter box
950
951void doScript(struct content *content, char commandString, struct event *event, void *blob)
952
953Define a command
954 name, argsDefinitionString, pointer to C function - void myCommand(struct box *box, char *command, struct ToyboxArguments *args, struct event *event, void *blob)
955
956Define a key
957 keyName, commandString
958
959Define a menu item
960 menuTitle, commandString
961
962Define a border widget
963 borderPosition, borderType, textOrCommand
964
965Define a popup
966 popupName, commandStub, thingThatMapsWidgetsToArgs
967
968Define a script callback
969 name, command, someKey
970
971---------------------------------------------
972
973Script interface.
974
975We might have multiple scripts running, but only one per box.
976 But what if a script wants to split it's box?
977They can't access our data structures, and we can't access theirs.
978All needs to be done via stdin/stdout plain text, which should all look like the commands in the rest of the system.
979
980Scripts can register simple callbacks on these events - key, menu, timer, box destroyed.
981The first three might want to return a damage list.
982The last means that the box wants to be destroyed, but the script gets a chance to clean up.
983
984Scripts need to be able to hook into the damage system, so there needs to be a text representation of damage areas.
985
986boxes <-> script
987
988Boxes knows which script is attached to which box.
989Registering a key for the box is optional, and it's only passed back if it exists.
990A split box gets no key, or can have the key optionally sent with the split command.
991 So how does the script deal with stuff coming from multiple boxes?
992
993<- keyForBox someKey
994<- registerKey keyName, commandString arguments
995<- registerKey keyName, callBackCommand arguments
996user hits a key
997 if the key has a command, execute it.
998 if the command is not one of ours, send it to the script instead
999-> callBackCommand arguments, someKey
1000 otherwise do our command
1001 else
1002 send the key event to the script.
1003-> handleEvent keyName, someKey
1004
1005 In any case, the script might want to change things in response.
1006 Note that it could do these at any time.
1007<- damage x, y, h, w
1008 line
1009 line
1010 line
1011 line
1012-> doneRedraw someKey
1013<- border borderPosition, borderType, textOrCommand
1014<- status line
1015
1016 *
1017 */
1018
1019
1020struct function
1021{
1022 name // Name for script purposes.
1023 description // Human name for the menus.
1024 type
1025 union
1026 {
1027 *scriptCallback
1028 *cFunction
1029 }
1030};
1031
1032struct command
1033{
1034 key // Note that any given context might have different keys for any given function.
1035 *function
1036};
1037
1038struct item
1039{
1040 type
1041 union
1042 {
1043 *command
1044 *menu
1045 }
1046}
1047
1048struct menu
1049{
1050 *items[] // Circular pointer definiton for sub menus.
1051}
1052
1053struct context // Somehow I get the feeling I'm having a failure of imagination here with the menus and function keys. Might be better to manage them seperately per box, but have common ones available? Nano might have a problem with this.
1054{
1055 *commands[] // The master list, the ones pointed to by the menu structs should be in this list.
1056 menu *menu // Can be NULL.
1057 menu *functionKeys // Can be NULL.
1058 // This can be used as the sub struct for various context types. Like viewer, editor, file browser, top, etc.
1059 // Could even be an object hierarchy, like generic editor, which Basic vi inherits from.
1060};
1061
1062char borderchars[][]
1063{
1064 // usual box graphic symbols
1065 '-|+',
1066 // ANSI code for inverse spaces.
1067}
1068
1069struct borderWidget
1070{
1071 text
1072 *clickFunction(int position)
1073}
1074
1075struct border
1076{
1077 *topLeftWidget
1078 *topRightWidget
1079 *bottomLeftWidget
1080 *bottomRightWidget
1081 *leftWidget
1082 *rightWidget
1083}
1084
1085struct damage
1086{
1087 X, Y, W, H // The rectangle to be redrawn.
1088 char **lines // Pointer to an array of text lines, or NULL.
1089 *damage // Perhaps a linked list might be in order, for fast redraws.
1090}
1091
1092struct content // For various instances of context types, in other words, the editor might have several files open, so one of these per file.
1093{
1094 minW, minH, maxW, maxH
1095 *context
1096 *handleEvents() // Should set the damage list if it needs a redraw, and flags if border or status line needs updating.
1097 // Keyboard / mouse events if the box did not handle them itself.
1098 // DrawAll event for when drawing the box for the first time, on reveals for coming out of full screen, or user hit the redraw key.
1099 // Scroll event if the content wants to handle that itself.
1100 // Timer event for things like top that might want to have this called regularly.
1101 *doneRedraw() // The box is done with it's redraw, so we can free the damage list or whatever now.
1102 *delete()
1103 // This can be used as the sub struct for various content types.
1104};
1105
1106struct contentData
1107{
1108 *border // Can be NULL.
1109 *statusLine // Text of the status line, or NULL if none.
1110 offsetX, offsetY, W, H // Offset and size within the content, coz box handles scrolling, usually.
1111 bool redrawStatus, redrawBorder
1112 *damage // Can be NULL. If not NULL after content->doneRedraw(), box will free it and it's children.
1113 void *data // The content controls this blob, it's specific to each box.
1114}
1115
1116struct box
1117{
1118 box *sub, *parent
1119 bool noBorderOnFullScreen
1120 bool horizontalSplit // Marks if it's horizontally or vertically split.
1121 splitProportion // proportion of this boxes part of the split, the sub box gets the rest.
1122 *content
1123 contentData // Data blob specific to this box, passed to each content function. For sharing contents, like a split pane editor for instance. Not a pointer, but the struct.
1124 X, Y, W, H // Position and size of the box itself, not the content. Calculated, but cached coz that might be needed for speed.
1125 cX, cY // Position of the content within the box. Calculated, but cached coz that might be needed for speed.
1126};
1127
1128box root; // Always a full screen, parent of the rest of the boxes, or the only box.
1129box current;
1130bool currentIsFullScreen;
diff --git a/src/boxes/BUGS.txt b/src/boxes/BUGS.txt
new file mode 100644
index 0000000..87c3d4b
--- /dev/null
+++ b/src/boxes/BUGS.txt
@@ -0,0 +1,6 @@
1xterm current box characters are wrong
2joe - ^K^D not working, but ^Kd is
3 Even odder, ^D works, all other ^K^? combinations work.
4F1 seems to not work, at least under xterm.
5
6should clear the command line prompt when not in use
diff --git a/src/boxes/README b/src/boxes/README
new file mode 100644
index 0000000..e01bb6b
--- /dev/null
+++ b/src/boxes/README
@@ -0,0 +1,34 @@
1boxes
2=====
3
4A test bed for a generic editor / pager thingy for the toybox project.
5
6The toybox project is at http://www.landley.net/code/toybox/ and boxes
7is covered by the same license that toybox is. Basically that's a two
8clause BSD license, but see the LICENSE file from toybox for details.
9
10This is a work in progress, proof of concept, playground, packaged up as
11one big toy, to be self contained until the mess is cleaned up. This
12"boxes" toy itself will go away, to be replaced by individual editor /
13pager toys and library bits. Nothing is set in stone, lots of mess
14inside, there's bugs, but at least it shows the general direction my
15mind is wandering in. As a bonus, it actually works, you can edit
16stuff.
17
18Please don't actually include this in toybox. Just look at it and sneer
19/ giggle, depending on your nature. Drop it into the toys directory to
20try it out, it's just one big toy.
21
22If you want to see how it can be used to build specific editors, start
23at the end and work backwards. Reading the lengthy comments at the
24beginning would also be useful.
25
26Toybox uses mecurial instead of git, but I keep all my stuff on github.
27Boxes will hopefully be incorporated into toybox in a highly altered
28form some day in the future. So this is just a temporary git repo for
29my convenience. If and when boxes migrates to toybox, this repo will be
30retired.
31
32
33BTW, toybox REQUIRES a README file here, a README.md file isn't good
34enough.
diff --git a/src/boxes/boxes.c b/src/boxes/boxes.c
new file mode 100644
index 0000000..4f542e1
--- /dev/null
+++ b/src/boxes/boxes.c
@@ -0,0 +1,2517 @@
1/* boxes.c - Generic editor development sandbox.
2 *
3 * Copyright 2012 David Seikel <won_fang@yahoo.com.au>
4 *
5 * Not in SUSv4. An entirely new invention, thus no web site either.
6 * See -
7 * http://pubs.opengroup.org/onlinepubs/9699919799/utilities/ex.html
8 * http://pubs.opengroup.org/onlinepubs/9699919799/utilities/more.html
9 * http://pubs.opengroup.org/onlinepubs/9699919799/utilities/sed.html
10 * http://pubs.opengroup.org/onlinepubs/9699919799/utilities/vi.html
11 * http://linux.die.net/man/1/less
12
13USE_BOXES(NEWTOY(boxes, "w#h#m(mode):a(stickchars)1", TOYFLAG_USR|TOYFLAG_BIN))
14
15config BOXES
16 bool "boxes"
17 default y
18 help
19 usage: boxes [-m|--mode mode] [-a|--stickchars] [-w width] [-h height]
20
21 Generic text editor and pager.
22
23 Mode selects which editor or text viewr it emulates, the choices are -
24 emacs is a microemacs type editor.
25 joe is a joe / wordstar type editor.
26 less is a less type pager.
27 mcedit (the default) is cooledit / mcedit type editor.
28 more is a more type pager.
29 nano is a nano / pico type editor.
30 vi is a vi type editor.
31
32 Stick chars means to use ASCII for the boxes instead of "graphics" characters.
33*/
34
35#include "toys.h"
36#include "lib/handlekeys.h"
37
38GLOBALS(
39 char *mode;
40 long h, w;
41)
42
43#define TT this.boxes
44
45#define FLAG_a 2
46#define FLAG_m 4
47#define FLAG_h 8
48#define FLAG_w 16
49
50
51/* This is trying to be a generic text editing, text viewing, and terminal
52 * handling system. The current code is a work in progress, and the design
53 * may change. Certainly at this moment it's only partly written. It is
54 * "usable" though, for a very small value of "usable". In the following
55 * I'll use "editors" to refer to the toys using this, though not all will
56 * be editors.
57 *
58 * The things it is targeting are - vi and more (part of the standards, so
59 * part of the toybox TODO), less (also on the toybox TODO), joe and
60 * wordstar (coz Rob said they would be good to include), nano (again Rob
61 * thinks it would be good and I agree), microemacs (to avoid religous
62 * wars), and mcedit (coz that's what I actually use). The ex editor comes
63 * along for the ride coz vi is basically a screen editor wrapper around
64 * the ex line editor. Sed might be supported coz I'll need to do basic
65 * editing functions that are common to the editors, and sed needs the same
66 * editing functions.
67 *
68 * I will also use this for a midnight commander clone as discussed on the
69 * mailing list. This would have things in common with emacs dired, so we
70 * might get that as well. Parts of this code could also be used for a
71 * file chooser, as used by some of the editors we are targeting. Finally,
72 * the terminal handling stuff might be useful for other toys, so should be
73 * generic in it's own right. Oh, screen is listed in the toybox TODO as
74 * "maybe", so I'll poke at that to.
75 *
76 * The basic building blocks are box, content, context, edit line, and
77 * view. A box is an on screen rectanglur area. Content is a file, and
78 * the text that is in that file. A context represents a particular editor
79 * type, it has key mappings and other similar stuff. The edit line is a
80 * single line where editing happens, it's similar to readline. A view is
81 * a view into a content, there can be many, it represents that portion of
82 * the content that is on screen right now.
83 *
84 * I plan on splitting these things up a bit so they can be used
85 * separately. Then I can make actually toybox libraries out of them. For
86 * now it's all one big file for ease of development.
87 *
88 * The screen is split into boxes, by default there are only two, the main
89 * text area and the command line at the bottom. Each box contains a view,
90 * and each view points to a content (file) for it's text. A content can
91 * have many views. Each content has a context (editor). There is only
92 * ever one edit line, it's the line that is being edited at the moment.
93 * The edit line moves within and between boxes (including the command
94 * line) as needed.
95 *
96 * The justification for boxes is that most of the editors we are trying to
97 * emulate include some splitting up of the screen area for various
98 * reasons, and some of them include a split window system as well. So
99 * this boxes concept covers command line, main editing area, split windows,
100 * menus, on screen display of command keys, file selection, and anything
101 * else that might be needed.
102 *
103 * To keep things simple boxes are organised as a binary tree of boxes.
104 * There is a root box, it's a global. Each box can have two sub boxes.
105 * Only the leaf nodes of the box tree are visible on the screen. Each box
106 * with sub boxes is split either horizontally or vertically. Navigating
107 * through the boxes goes depth first.
108 *
109 * A content keeps track of a file and it's text. Each content also has a
110 * context, which is a collection of the things that define a particular
111 * editor. (I might move the context pointer from content to view, makes
112 * more sense I think.)
113 *
114 * A context is the heart of the generic part of the system. Any given
115 * toybox command that uses this system would basically define a context
116 * and pass that to the rest of the system. See the end of this file for a
117 * few examples. A context holds a list of command to C function mappings,
118 * key to command mappings, and a list of modes.
119 *
120 * Most of the editors targetted include a command line where the user
121 * types in editor commands, and each of those editors has different
122 * commands. They would mostly use the same editing C functions though, or
123 * short wrappers around them. The vi context at the end of this file is a
124 * good example, it has a bunch of short C wrappers around some editing
125 * functions, or uses standard C editing functions directly. So a context
126 * has an editor command to C function mapping.
127 *
128 * The editors respond to keystrokes, and those keystrokes invoke editor
129 * commands, often in a modal way. So there are keystroke to editor
130 * command mappings. To cater for editing modes, each context has a list
131 * of modes, and each mode can have key to command mappings, as well as
132 * menu to command mappings, and a list of displayed key/command pairs.
133 * Menu and key/command pair display is not written yet. Most editors have
134 * a system for remapping key to command mappings, that's not supported
135 * yet. Emacs has a heirarchy of key to command mappings, that's not
136 * supported yet. Some twiddling of the current design would be needed for
137 * those.
138 *
139 * The key mappings used can be multiple keystrokes in a sequence, the
140 * system caters for that. Some can be multi byte like function keys, and
141 * even different strings of bytes depending on the terminal type. To
142 * simplify this, there is a table that maps various terminals ideas of
143 * special keys to key names, and the mapping of keys to commands uses
144 * those key names.
145 *
146 * A view represents the on screen visible portion of a content within a
147 * box. To cater for split window style editors, a content can have many
148 * views. It deals with keeping track of what's shown on screen, mapping
149 * the on screen representation of the text to the stored text during input
150 * and output. Each box holds one view.
151 *
152 * The edit line is basically a movable readline. There are editing C
153 * functions for moving it up and down lines within a view, and for
154 * shifting the edit line to some other box. So an editor would map the
155 * cursor keys for "up a line" and "down a line" to these C functions for
156 * example. Actual readline style functionality is just the bottom command
157 * box being a single line view, with the history file loaded into it's
158 * content, and the Enter key mapped to the editor contexts "do this
159 * command" function. Using most of the system with not much special casing.
160 *
161 *
162 * I assume that there wont be a terribly large number of boxes.
163 * Things like minimum box sizes, current maximum screen sizes, and the fact
164 * that they all have to be on the screen mean that this assumption should
165 * be safe. It's likely that most of the time there will be only a few at
166 * most. The point is there is a built in limit, there's only so many non
167 * overlapping boxes with textual contents that you can squeeze onto one
168 * terminal screen at once.
169 *
170 * I assume that there will only be one command line, no matter how many boxes,
171 * and that the command line is for the current box.
172 *
173 * I use a wide screen monitor and small font.
174 * My usual terminal is 104 x 380 characters.
175 * There will be people with bigger monitors and smaller fonts.
176 * So use sixteen bits for storing screen positions and the like.
177 * Eight bits wont cut it.
178 *
179 *
180 * These are the escape sequences we send -
181 * \x1B[m reset attributes and colours
182 * \x1B[1m turn on bold
183 * \x1B[%d;%dH move cursor
184 * Plus some experimentation with turning on mouse reporting that's not
185 * currently used.
186 *
187 *
188 * TODO - disentangle boxes from views.
189 *
190 * TODO - should split this up into editing, UI, and boxes parts,
191 * so the developer can leave out bits they are not using.
192 *
193 * TODO - Show status line instead of command line when it's not being edited.
194 *
195 * TODO - should review it all for UTF8 readiness. Think I can pull that off
196 * by keeping everything on the output side as "screen position", and using
197 * the formatter to sort out the input to output mapping.
198 *
199 * TODO - see if there are any simple shortcuts to avoid recalculating
200 * everything all the time. And to avoid screen redraws.
201 */
202
203/* Robs "It's too big" lament.
204
205> So when you give me code, assume I'm dumber than you. Because in this
206> context, I am. I need bite sized pieces, each of which I can
207> understand in its entirety before moving on to the next.
208
209As I mentioned in my last email to the Aboriginal linux list, I wrote
210the large blob so I could see how the ideas all work to do the generic
211editor stuff and other things. It kinda needs to be that big to do
212anything that is actually useful. So, onto musings about cutting it up
213into bite sized bits...
214
215You mentioned on the Aboriginal Linux list that you wanted a
216"readline", and you listed some features. My reply was that you had
217basically listed the features of my "basic editor". The main
218difference between "readline" and a full screen editor, is that the
219editor is fullscreen, while the "readline" is one line. They both have
220to deal with a list of lines, going up and down through those lines,
221editing the contents of those lines, one line at a time. For persistent
222line history, they both have to save and load those lines to a file.
223Other than that "readline" adds other behaviour, like moving the
224current line to the end when you hit return and presenting that line to
225the caller (here's the command). So just making "readline" wont cut
226out much of the code. In fact, my "readline" is really just a thin
227wrapper around the rest of the code.
228
229Starting from the other end of the size spectrum, I guess "find out
230terminal size" is a small enough bite sized chunk. To actually do
231anything useful with that you would probably start to write stuff I've
232already written though. Would be better off just using the stuff I've
233already written. On the other hand, maybe that would be useful for
234"ls" listings and the like, then we can start with just that bit?
235
236I guess the smallest useful command I have in my huge blob of code
237would be "more". I could even cut it down more. Show a page of text,
238show the next page of text when you hit the space bar, quit when you
239hit "q". Even then, I would estimate it would only cut out a third of
240the code.
241
242On the other hand, there's a bunch of crap in that blob that is for
243future plans, and is not doing anything now.
244
245In the end though, breaking it up into suitable bite sized bits might
246be almost as hard as writing it in the first place. I'll see what I
247can do, when I find some time. I did want to break it up into three
248bits anyway, and there's a TODO to untangle a couple of bits that are
249currently too tightly coupled.
250
251*/
252
253/* Robs contradiction
254
255On Thu, 27 Dec 2012 06:06:53 -0600 Rob Landley <rob@landley.net> wrote:
256
257> On 12/27/2012 12:56:07 AM, David Seikel wrote:
258> > On Thu, 27 Dec 2012 00:37:46 -0600 Rob Landley <rob@landley.net>
259> > wrote:
260> > > Since ls isn't doiing any of that, probing would just be awkward
261> > > so toysh should set COLUMNS to a sane value. The problem is,
262> > > we're not using toysh yet because it's still just a stub...
263> >
264> > I got some or all of that terminal sizing stuff in my editor thingy
265> > already if I remember correctly. I remember you wanted me to cut it
266> > down into tiny bits to start with, so you don't have to digest the
267> > huge lump I sent.
268>
269> Basically what I'd like is a self-contained line reader. More or less
270> a getline variant that can handle cursoring left and right to insert
271> stuff, handle backspace past a wordwrap (which on unix requires
272> knowing the screen width and your current cursor position), and one
273> that lets up/down escape sequences be hooked for less/more screen
274> scrolling, vi line movement, shell command history, and so on.
275
276Which is most of an editor already, so how to break it up into bite
277sized morsels?
278
279> There's sort of three levels here, the first is "parse raw input,
280> assembling escape sequences into cursor-left and similar as
281> necessary". (That's the one I had to redo in busybox vi because they
282> didn't do it right.)
283>
284> The second is "edit a line of text, handling cursoring _within_ the
285> line". That's the better/interactive getline().
286>
287> The third is handling escapes from that line triggering behavior in
288> the larger command (cursor up and cursor down do different things in
289> less, in vi, and in shell command history).
290>
291> The fiddly bit is that terminal_size() sort of has to integrate with
292> all of these. Possibly I need to have width and height be members of
293> struct toy_context. Or have the better_getline() take a pointer to a
294> state structure it can update, containing that info...
295
296*/
297
298
299static char *borderChars[][6] =
300{
301 {"-", "|", "+", "+", "+", "+"}, // "stick" characters.
302 {"\xE2\x94\x80", "\xE2\x94\x82", "\xE2\x94\x8C", "\xE2\x94\x90", "\xE2\x94\x94", "\xE2\x94\x98"}, // UTF-8
303 {"\x71", "\x78", "\x6C", "\x6B", "\x6D", "\x6A"}, // VT100 alternate character set.
304 {"\xC4", "\xB3", "\xDA", "\xBF", "\xC0", "\xD9"} // DOS
305};
306
307static char *borderCharsCurrent[][6] =
308{
309 {"=", "#", "+", "+", "+", "+"}, // "stick" characters.
310 {"\xE2\x95\x90", "\xE2\x95\x91", "\xE2\x95\x94", "\xE2\x95\x97", "\xE2\x95\x9A", "\xE2\x95\x9D"}, // UTF-8
311 {"\x71", "\x78", "\x6C", "\x6B", "\x6D", "\x6A"}, // VT100 alternate character set has none of these. B-(
312 {"\xCD", "\xBA", "\xC9", "\xBB", "\xC8", "\xBC"} // DOS
313};
314
315
316typedef struct _box box;
317typedef struct _view view;
318
319typedef void (*boxFunction) (box *box);
320typedef void (*eventHandler) (view *view);
321
322struct function
323{
324 char *name; // Name for script purposes.
325 char *description; // Human name for the menus.
326 char type;
327 union
328 {
329 eventHandler handler;
330 char *scriptCallback;
331 };
332};
333
334struct keyCommand
335{
336 char *key, *command;
337};
338
339struct item
340{
341 char *text; // What's shown to humans.
342 struct key *key; // Shortcut key while the menu is displayed.
343 // If there happens to be a key bound to the same command, the menu system should find that and show it to.
344 char type;
345 union
346 {
347 char *command;
348 struct item *items; // An array of next level menu items.
349 };
350};
351
352struct borderWidget
353{
354 char *text, *command;
355};
356
357// TODO - a generic "part of text", and what is used to define them.
358// For instance - word, line, paragraph, section.
359// Each context can have a collection of these.
360
361struct mode
362{
363 struct keyCommand *keys; // An array of key to command mappings.
364 struct item *items; // An array of top level menu items.
365 struct item *functionKeys; // An array of single level "menus". Used to show key commands.
366 uint8_t flags; // commandMode.
367};
368
369/*
370Have a common menu up the top.
371 MC has a menu that changes per mode.
372 Nano has no menu.
373Have a common display of certain keys down the bottom.
374 MC is one row of F1 to F10, but changes for edit / view / file browse. But those are contexts here.
375 Nano is 12 random Ctrl keys, possibly in two lines, that changes depending on the editor mode, like editing the prompt line for various reasons, help.
376*/
377struct context // Defines a context for content. Text viewer, editor, file browser for instance.
378{
379 struct function *commands; // The master list, the ones pointed to by the menus etc should be in this list.
380 struct mode *modes; // A possible empty array of modes, indexed by the view.
381 // OR might be better to have these as a linked list, so that things like Emacs can have it's mode keymap hierarcy.
382 eventHandler handler; // TODO - Might be better to put this in the modes. I think vi will need that.
383 // Should set the damage list if it needs a redraw, and flags if border or status line needs updating.
384 // Keyboard / mouse events if the box did not handle them itself.
385 // DrawAll event for when drawing the box for the first time, on reveals for coming out of full screen, or user hit the redraw key.
386 // Scroll event if the content wants to handle that itself.
387 // Timer event for things like top that might want to have this called regularly.
388 boxFunction doneRedraw; // The box is done with it's redraw, so we can free the damage list or whatever now.
389 boxFunction delete;
390 // This can be used as the sub struct for various context types. Like viewer, editor, file browser, top, etc.
391 // Could even be an object hierarchy, like generic editor, which Basic vi inherits from.
392 // Or not, since the commands might be different / more of them.
393};
394
395// TODO - might be better off just having a general purpose "widget" which includes details of where it gets attached.
396// Status lines can have them to.
397struct border
398{
399 struct borderWidget *topLeft, *topMiddle, *topRight;
400 struct borderWidget *bottomLeft, *bottomMiddle, *bottomRight;
401 struct borderWidget *left, *right;
402};
403
404struct line
405{
406 struct line *next, *prev;
407 uint32_t length; // Careful, this is the length of the allocated memory for real lines, but the number of lines in the header node.
408 char *line; // Should be blank for the header.
409};
410
411struct damage
412{
413 struct damage *next; // A list for faster draws?
414 uint16_t X, Y, W, H; // The rectangle to be redrawn.
415 uint16_t offset; // Offest from the left for showing lines.
416 struct line *lines; // Pointer to a list of text lines, or NULL.
417 // Note - likely a pointer into the middle of the line list in a content.
418};
419
420struct content // For various instances of context types.
421 // Editor / text viewer might have several files open, so one of these per file.
422 // MC might have several directories open, one of these per directory. No idea why you might want to do this. lol
423{
424 struct context *context;
425 char *name, *file, *path;
426 struct line lines;
427// file type
428// double linked list of bookmarks, pointer to line, character position, length (or ending position?), type, blob for types to keep context.
429 uint16_t minW, minH, maxW, maxH;
430 uint8_t flags; // readOnly, modified.
431 // This can be used as the sub struct for various content types.
432};
433
434struct _view
435{
436 struct content *content;
437 box *box;
438 struct border *border; // Can be NULL.
439 char *statusLine; // Text of the status line, or NULL if none.
440 int mode; // For those contexts that can be modal. Normally used to index the keys, menus, and key displays.
441 struct damage *damage; // Can be NULL. If not NULL after context->doneRedraw(), box will free it and it's children.
442 // TODO - Gotta be careful of overlapping views.
443 void *data; // The context controls this blob, it's specific to each box.
444 uint32_t offsetX, offsetY; // Offset within the content, coz box handles scrolling, usually.
445 uint16_t X, Y, W, H; // Position and size of the content area within the box. Calculated, but cached coz that might be needed for speed.
446 uint16_t cX, cY; // Cursor position within the content.
447 uint16_t iX, oW; // Cursor position inside the lines input text, in case the formatter makes it different, and output length.
448 char *output; // The current line formatted for output.
449 uint8_t flags; // redrawStatus, redrawBorder;
450
451 // Assumption is that each box has it's own editLine (likely the line being edited), or that there's a box that is just the editLine (emacs minibuffer, and command lines for other proggies).
452 struct line *line; // Pointer to the current line, might be the only line.
453 char *prompt; // Optional prompt for the editLine.
454
455// Display mode / format hook.
456// view specific bookmarks, including highlighted block and it's type.
457// Linked list of selected lines for a filtered view, or processing only those lines.
458// Linked list of pointers to struct keyCommand, for emacs keymaps hierarchy, and anything similar in other editors. Plus some way of dealing with emacs minor mode keymaps.
459};
460
461struct _box
462{
463 box *sub1, *sub2, *parent;
464 view *view; // This boxes view into it's content. For sharing contents, like a split pane editor for instance, there might be more than one box with this content, but a different view.
465 // If it's just a parent box, it wont have this, so just make it a damn pointer, that's the simplest thing. lol
466 // TODO - Are parent boxes getting a view anyway?
467 uint16_t X, Y, W, H; // Position and size of the box itself, not the content. Calculated, but cached coz that might be needed for speed.
468 float split; // Ratio of sub1's part of the split, the sub2 box gets the rest.
469 uint8_t flags; // Various flags.
470};
471
472
473// Sometimes you just can't avoid circular definitions.
474void drawBox(box *box);
475
476
477#define BOX_HSPLIT 1 // Marks if it's a horizontally or vertically split.
478#define BOX_BORDER 2 // Mark if it has a border, often full screen boxes wont.
479
480static int overWriteMode;
481static box *rootBox; // Parent of the rest of the boxes, or the only box. Always a full screen.
482static box *currentBox;
483static view *commandLine;
484static int commandMode;
485
486#define MEM_SIZE 128 // Chunk size for line memory allocation.
487
488// Inserts the line after the given line, or at the end of content if no line.
489struct line *addLine(struct content *content, struct line *line, char *text, uint32_t length)
490{
491 struct line *result = NULL;
492 uint32_t len;
493
494 if (!length)
495 length = strlen(text);
496 // Round length up.
497 len = (((length + 1) / MEM_SIZE) + 1) * MEM_SIZE;
498 result = xzalloc(sizeof(struct line));
499 result->line = xzalloc(len);
500 result->length = len;
501 strncpy(result->line, text, length);
502
503 if (content)
504 {
505 if (!line)
506 line = content->lines.prev;
507
508 result->next = line->next;
509 result->prev = line;
510
511 line->next->prev = result;
512 line->next = result;
513
514 content->lines.length++;
515 }
516 else
517 {
518 result->next = result;
519 result->prev = result;
520 }
521
522 return result;
523}
524
525void freeLine(struct content *content, struct line *line)
526{
527 line->next->prev = line->prev;
528 line->prev->next = line->next;
529 if (content)
530 content->lines.length--;
531 free(line->line);
532 free(line);
533}
534
535void loadFile(struct content *content)
536{
537 int fd = open(content->path, O_RDONLY);
538
539 if (-1 != fd)
540 {
541 char *temp = NULL;
542 long len = 0;
543
544 do
545 {
546 // TODO - get_line() is slow, and wont help much with DOS and Mac line endings.
547 temp = get_line(fd);
548 if (temp)
549 addLine(content, NULL, temp, len);
550 } while (temp);
551 close(fd);
552 }
553}
554
555// TODO - load and save should be able to deal with pipes, and with loading only parts of files, to load more parts later.
556
557void saveFile(struct content *content)
558{
559// TODO - Should do "Save as" as well. Which is just a matter of changing content->path before calling this.
560 int fd;
561
562 fd = open(content->path, O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH);
563
564 if (-1 != fd)
565 {
566 struct line *line = content->lines.next;
567
568 while (&(content->lines) != line) // We are at the end if we have wrapped to the beginning.
569 {
570 write(fd, line->line, strlen(line->line));
571 write(fd, "\n", 1);
572 line = line->next;
573 }
574 close(fd);
575 }
576 else
577 {
578 fprintf(stderr, "Can't open file %s\n", content->path);
579 exit(1);
580 }
581}
582
583struct content *addContent(char *name, struct context *context, char *filePath)
584{
585 struct content *result = xzalloc(sizeof(struct content));
586
587 result->lines.next = &(result->lines);
588 result->lines.prev = &(result->lines);
589 result->name = strdup(name);
590 result->context = context;
591
592 if (filePath)
593 {
594 result->path = strdup(filePath);
595 loadFile(result);
596 }
597
598 return result;
599}
600
601// General purpose line moosher. Used for appends, inserts, overwrites, and deletes.
602// TODO - should have the same semantics as mooshStrings, only it deals with whole lines in double linked lists.
603// We need content so we can adjust it's number of lines if needed.
604void mooshLines(struct content *content, struct line *result, struct line *moosh, uint16_t index, uint16_t length, int insert)
605{
606}
607
608// General purpose string moosher. Used for appends, inserts, overwrites, and deletes.
609void mooshStrings(struct line *result, char *moosh, uint16_t index, uint16_t length, int insert)
610{
611 char *c, *pos;
612 int limit = strlen(result->line);
613 int mooshLen = 0, resultLen;
614
615 if (moosh)
616 mooshLen = strlen(moosh);
617
618 /*
619 * moosh == NULL a deletion
620 * length == 0 simple insertion
621 * length < mooshlen delete some, insert moosh
622 * length == mooshlen exact overwrite.
623 * length > mooshlen delete a lot, insert moosh
624 */
625
626 mooshLen -= length;
627 resultLen = limit + mooshLen;
628
629 // If we need more space, allocate more.
630 if (resultLen > result->length)
631 {
632 result->length = resultLen + MEM_SIZE;
633 result->line = xrealloc(result->line, result->length);
634 }
635
636 if (limit <= index) // At end, just add to end.
637 {
638 // TODO - Possibly add spaces to pad out to where index is?
639 // Would be needed for "go beyond end of line" and "column blocks".
640 // Both of those are advanced editing.
641 index = limit;
642 insert = 1;
643 }
644
645 pos = &(result->line[index]);
646
647 if (insert) // Insert / delete before current character, so move it and the rest up / down mooshLen bytes.
648 {
649 if (0 < mooshLen) // Gotta move things up.
650 {
651 c = &(result->line[limit]);
652 while (c >= pos)
653 {
654 *(c + mooshLen) = *c;
655 c--;
656 }
657 }
658 else if (0 > mooshLen) // Gotta move things down.
659 {
660 c = pos;
661 while (*c)
662 {
663 *c = *(c - mooshLen); // A double negative.
664 c++;
665 }
666 }
667 }
668
669 if (moosh)
670 {
671 c = moosh;
672 do
673 {
674 *pos++ = *c++;
675 }
676 while (*c);
677 }
678}
679
680// TODO - Should draw the current border in green, the text as default (or highlight / bright).
681// Then allow one other box to be red bordered (MC / dired destination box).
682// All other boxes with dark gray border, and dim text.
683void drawLine(int y, int start, int end, char *left, char *internal, char *contents, char *right, int current)
684{
685 int size = strlen(internal);
686 int len = (end - start) * size, x = 0;
687 char line[len + 1];
688
689 if ('\0' != left[0]) // Assumes that if one side has a border, then so does the other.
690 len -= 2 * size;
691
692 if (contents)
693 {
694 // strncpy wont add a null at the end if the source is longer, but will pad with nulls if source is shorter.
695 // So it's best to put a safety null in first.
696 line[len] = '\0';
697 strncpy(line, contents, len);
698 // Make sure the following while loop pads out line with the internal character.
699 x = strlen(line);
700 }
701 while (x < len)
702 {
703 strcpy(&line[x], internal);
704 x += size;
705 }
706 line[x++] = '\0';
707 if ('\0' == left[0]) // Assumes that if one side has a border, then so does the other.
708 {
709 if (current)
710 printf("\x1B[1m\x1B[%d;%dH%s\x1B[m", y + 1, start + 1, line);
711 else
712 printf("\x1B[m\x1B[%d;%dH%s", y + 1, start + 1, line);
713 }
714 else
715 {
716 if (current)
717 printf("\x1B[1m\x1B[%d;%dH%s%s%s\x1B[m", y + 1, start + 1, left, line, right);
718 else
719 printf("\x1B[m\x1B[%d;%dH%s%s%s", y + 1, start + 1, left, line, right);
720 }
721}
722
723void formatCheckCursor(view *view, long *cX, long *cY, char *input)
724{
725 int i = 0, o = 0, direction = (*cX) - view->cX;
726
727 // Adjust the cursor if needed, depending on the contents of the line, and the direction of travel.
728 while (input[i])
729 {
730 // When o is equal to the cX position, update the iX position to be where i is.
731 if ('\t' == input[i])
732 {
733 int j = 8 - (i % 8);
734
735 // Check if the cursor is in the middle of the tab.
736 if (((*cX) > o) && ((*cX) < (o + j)))
737 {
738 if (0 <= direction)
739 {
740 *cX = (o + j);
741 view->iX = i + 1;
742 }
743 else
744 {
745 *cX = o;
746 view->iX = i;
747 }
748 }
749 o += j;
750 }
751 else
752 {
753 if ((*cX) == o)
754 view->iX = i;
755 o++;
756 }
757 i++;
758 }
759 // One more check in case the cursor is at the end of the line.
760 if ((*cX) == o)
761 view->iX = i;
762}
763
764// TODO - Should convert control characters to reverse video, and deal with UTF8.
765
766/* FIXME - We get passed a NULL input, apparently when the file length is close to the screen length. -
767> On Thu, 6 Sep 2012 11:56:17 +0800 Roy Tam <roytam@gmail.com> wrote:
768>
769> > 2012/9/6 David Seikel <onefang@gmail.com>:
770> > >> Program received signal SIGSEGV, Segmentation fault.
771> > >> formatLine (view=0x8069168, input=0x0, output=0x806919c)
772> > >> at toys/other/boxes.c:843
773> > >> 843 int len = strlen(input), i = 0, o = 0;
774> > >> (gdb) bt
775> > >> #0 formatLine (view=0x8069168, input=0x0, output=0x806919c)
776> > >> at toys/other/boxes.c:843
777> > >> #1 0x0804f1dd in moveCursorAbsolute (view=0x8069168, cX=0,
778> > >> cY=10, sX=0, sY=0) at toys/other/boxes.c:951
779> > >> #2 0x0804f367 in moveCursorRelative (view=0x8069168, cX=0,
780> > >> cY=10, sX=0, sY=0) at toys/other/boxes.c:1011
781> > >> #3 0x0804f479 in upLine (view=0x8069168, event=0x0) at
782> > >> toys/other/boxes.c:1442 #4 0x0804fb63 in handleKey
783> > >> (view=0x8069168, i=2, keyName=<optimized out>, buffer=0xbffffad8
784> > >> "\033[A") at toys/other/boxes.c:1593 #5 0x0805008d in editLine
785> > >> (view=0x8069168, X=-1, Y=-1, W=-1, H=-1) at
786> > >> toys/other/boxes.c:1785 #6 0x08050288 in boxes_main () at
787> > >> toys/other/boxes.c:2482 #7 0x0804b262 in toy_exec
788> > >> (argv=0xbffffd58) at main.c:104 #8 0x0804b29d in toybox_main ()
789> > >> at main.c:118 #9 0x0804b262 in toy_exec (argv=0xbffffd54) at
790> > >> main.c:104 #10 0x0804b29d in toybox_main () at main.c:118
791> > >> #11 0x0804affa in main (argc=5, argv=0xbffffd54) at main.c:159
792> > >
793> > > No segfault here when I try that, nor with different files. As I
794> > > said, it has bugs. I have seen other cases before when NULL lines
795> > > are passed around near that code. Guess I've not got them all.
796> > > Might help to mention what terminal proggy you are using, it's
797> > > size in characters, toybox version, OS, etc. Something is
798> > > different between you and me.
799> >
800> > Terminal Program: PuTTY
801> > Windows size: 80 * 25 chars
802> > Toybox version: hg head
803> > OS: Linux 3.2 (Debian wheezy)
804> > File: Toybox LICENSE file
805>
806> I'll install PuTTY, try toybox hg head, and play with them later, see
807> if I can reproduce that segfault. I use the last released tarball of
808> toybox, an old Ubuntu, and a bunch of other terminal proggies. I did
809> not know that PuTTY had been ported to Unix.
810>
811> But as I mentioned, not interested in a bug hunt right now. That will
812> be more appropriate when it's less of a "sandbox for testing the
813> ideas" and more of a "good enough to bother using". Always good to
814> have other terminals for testing though.
815
816Seems to be sensitive to the number of lines. On my usual huge
817roxterm, using either toybox 0.4 or hg head, doing this -
818
819./toybox boxes -m less LICENSE -h 25
820
821and just hitting down arrow until you get to the bottom of the page
822segfaults as well. -h means "pretend the terminal is that many lines
823high", there's a similar -w as well. 80x25 in any other terminal did
824the same. For that particular file (17 lines long), any value of -h
825between 19 and 34 will segfault after some moving around. -h 35 makes
826it segfault straight away. Less than 19 or over 35 is fine. Longer
827files don't have that problem, though I suspect it will happen on
828shortish files where the number of lines is about the length of the
829screen.
830
831I guess that somewhere I'm doing maths wrong and getting an out of
832bounds line number when the file length is close to the screen length.
833I'll make note of that, and fix it when I next get time to beat on
834boxes.
835
836Thanks for reporting it Roy.
837*/
838
839int formatLine(view *view, char *input, char **output)
840{
841 int len = strlen(input), i = 0, o = 0;
842
843 *output = xrealloc(*output, len + 1);
844
845 while (input[i])
846 {
847 if ('\t' == input[i])
848 {
849 int j = 8 - (i % 8);
850
851 *output = xrealloc(*output, len + j + 1);
852 for (; j; j--)
853 {
854 (*output)[o++] = ' ';
855 len++;
856 }
857 len--; // Not counting the actual tab character itself.
858 }
859 else
860 (*output)[o++] = input[i];
861 i++;
862 }
863 (*output)[o++] = '\0';
864
865 return len;
866}
867
868void drawContentLine(view *view, int y, int start, int end, char *left, char *internal, char *contents, char *right, int current)
869{
870 char *temp = NULL;
871 int offset = view->offsetX, len;
872
873 if (contents == view->line->line)
874 {
875 view->oW = formatLine(view, contents, &(view->output));
876 temp = view->output;
877 len = view->oW;
878 }
879 else // Only time we are not drawing the current line, and only used for drawing the entire page.
880 len = formatLine(NULL, contents, &(temp));
881
882 if (offset > len)
883 offset = len;
884 drawLine(y, start, end, left, internal, &(temp[offset]), right, current);
885}
886
887void updateLine(view *view)
888{
889 int y, len;
890
891 // Coz things might change out from under us, find the current view. Again.
892 if (commandMode) view = commandLine;
893 else view = currentBox->view;
894
895 // TODO - When doing scripts and such, might want to turn off the line update until finished.
896 // Draw the prompt and the current line.
897 y = view->Y + (view->cY - view->offsetY);
898 len = strlen(view->prompt);
899 drawLine(y, view->X, view->X + view->W, "", " ", view->prompt, "", 0);
900 drawContentLine(view, y, view->X + len, view->X + view->W, "", " ", view->line->line, "", 1);
901 // Move the cursor.
902 printf("\x1B[%d;%dH", y + 1, view->X + len + (view->cX - view->offsetX) + 1);
903 fflush(stdout);
904}
905
906void doCommand(view *view, char *command)
907{
908 if (command)
909 {
910 struct function *functions = view->content->context->commands;
911 int i;
912
913// TODO - Some editors have a shortcut command concept. The smallest unique first part of each command will match, as well as anything longer.
914// A further complication is if we are not implementing some commands that might change what is "shortest unique prefix".
915
916 for (i = 0; functions[i].name; i++)
917 {
918 if (strcmp(functions[i].name, command) == 0)
919 {
920 if (functions[i].handler)
921 {
922 functions[i].handler(view);
923 updateLine(view);
924 }
925 break;
926 }
927 }
928 }
929}
930
931int moveCursorAbsolute(view *view, long cX, long cY, long sX, long sY)
932{
933 struct line *newLine = view->line;
934 long oX = view->offsetX, oY = view->offsetY;
935 long lX = view->oW, lY = view->content->lines.length - 1;
936 long nY = view->cY;
937 uint16_t w = view->W - 1, h = view->H - 1;
938 int moved = 0, updatedY = 0, endOfLine = 0;
939
940 // Check if it's still within the contents.
941 if (0 > cY) // Trying to move before the beginning of the content.
942 cY = 0;
943 else if (lY < cY) // Trying to move beyond end of the content.
944 cY = lY;
945 if (0 > cX) // Trying to move before the beginning of the line.
946 {
947 // See if we can move to the end of the previous line.
948 if (view->line->prev != &(view->content->lines))
949 {
950 cY--;
951 endOfLine = 1;
952 }
953 else
954 cX = 0;
955 }
956 else if (lX < cX) // Trying to move beyond end of line.
957 {
958 // See if we can move to the begining of the next line.
959 if (view->line->next != &(view->content->lines))
960 {
961 cY++;
962 cX = 0;
963 }
964 else
965 cX = lX;
966 }
967
968 // Find the new line.
969 while (nY != cY)
970 {
971 updatedY = 1;
972 if (nY < cY)
973 {
974 newLine = newLine->next;
975 nY++;
976 if (view->content->lines.prev == newLine) // We are at the end if we have wrapped to the beginning.
977 break;
978 }
979 else
980 {
981 newLine = newLine->prev;
982 nY--;
983 if (view->content->lines.next == newLine) // We are at the end if we have wrapped to the beginning.
984 break;
985 }
986 }
987 cY = nY;
988
989 // Check if we have moved past the end of the new line.
990 if (updatedY)
991 {
992 // Format it.
993 view->oW = formatLine(view, newLine->line, &(view->output));
994 if (view->oW < cX)
995 endOfLine = 1;
996 if (endOfLine)
997 cX = view->oW;
998 }
999
1000 // Let the formatter decide if it wants to adjust things.
1001 // It's up to the formatter to deal with things if it changes cY.
1002 // On the other hand, changing cX is it's job.
1003 formatCheckCursor(view, &cX, &cY, newLine->line);
1004
1005 // Check the scrolling.
1006 lY -= view->H - 1;
1007 oX += sX;
1008 oY += sY;
1009
1010 if (oY > cY) // Trying to move above the box.
1011 oY += cY - oY;
1012 else if ((oY + h) < cY) // Trying to move below the box
1013 oY += cY - (oY + h);
1014 if (oX > cX) // Trying to move to the left of the box.
1015 oX += cX - oX;
1016 else if ((oX + w) <= cX) // Trying to move to the right of the box.
1017 oX += cX - (oX + w);
1018
1019 if (oY < 0)
1020 oY = 0;
1021 if (oY >= lY)
1022 oY = lY;
1023 if (oX < 0)
1024 oX = 0;
1025 // TODO - Should limit oX to less than the longest line, minus box width.
1026 // Gonna be a pain figuring out what the longest line is.
1027 // On the other hand, don't think that will be an actual issue unless "move past end of line" is enabled, and that's an advanced editor thing.
1028 // Though still might want to do that for the longest line on the new page to be.
1029
1030 if ((view->cX != cX) || (view->cY != cY))
1031 moved = 1;
1032
1033 // Actually update stuff, though some have been done already.
1034 view->cX = cX;
1035 view->cY = cY;
1036 view->line = newLine;
1037
1038 // Handle scrolling.
1039 if ((view->offsetX != oX) || (view->offsetY != oY))
1040 {
1041 view->offsetX = oX;
1042 view->offsetY = oY;
1043
1044 if (view->box)
1045 drawBox(view->box);
1046 }
1047
1048 return moved;
1049}
1050
1051int moveCursorRelative(view *view, long cX, long cY, long sX, long sY)
1052{
1053 return moveCursorAbsolute(view, view->cX + cX, view->cY + cY, sX, sY);
1054}
1055
1056void sizeViewToBox(box *box, int X, int Y, int W, int H)
1057{
1058 uint16_t one = 1, two = 2;
1059
1060 if (!(box->flags & BOX_BORDER))
1061 {
1062 one = 0;
1063 two = 0;
1064 }
1065 box->view->X = X;
1066 box->view->Y = Y;
1067 box->view->W = W;
1068 box->view->H = H;
1069 if (0 > X) box->view->X = box->X + one;
1070 if (0 > Y) box->view->Y = box->Y + one;
1071 if (0 > W) box->view->W = box->W - two;
1072 if (0 > H) box->view->H = box->H - two;
1073}
1074
1075view *addView(char *name, struct context *context, char *filePath, uint16_t X, uint16_t Y, uint16_t W, uint16_t H)
1076{
1077 view *result = xzalloc(sizeof(struct _view));
1078
1079 result->X = X;
1080 result->Y = Y;
1081 result->W = W;
1082 result->H = H;
1083
1084 result->content = addContent(name, context, filePath);
1085 result->prompt = xzalloc(1);
1086 // If there was content, format it's first line as usual, otherwise create an empty first line.
1087 if (result->content->lines.next != &(result->content->lines))
1088 {
1089 result->line = result->content->lines.next;
1090 result->oW = formatLine(result, result->line->line, &(result->output));
1091 }
1092 else
1093 {
1094 result->line = addLine(result->content, NULL, "\0", 0);
1095 result->output = xzalloc(1);
1096 }
1097
1098 return result;
1099}
1100
1101box *addBox(char *name, struct context *context, char *filePath, uint16_t X, uint16_t Y, uint16_t W, uint16_t H)
1102{
1103 box *result = xzalloc(sizeof(struct _box));
1104
1105 result->X = X;
1106 result->Y = Y;
1107 result->W = W;
1108 result->H = H;
1109 result->view = addView(name, context, filePath, X, Y, W, H);
1110 result->view->box = result;
1111 sizeViewToBox(result, X, Y, W, H);
1112
1113 return result;
1114}
1115
1116void freeBox(box *box)
1117{
1118 if (box)
1119 {
1120 freeBox(box->sub1);
1121 freeBox(box->sub2);
1122 if (box->view)
1123 {
1124 // In theory the line should not be part of the content if there is no content, so we should free it.
1125 if (!box->view->content)
1126 freeLine(NULL, box->view->line);
1127 free(box->view->prompt);
1128 free(box->view->output);
1129 free(box->view);
1130 }
1131 free(box);
1132 }
1133}
1134
1135void drawBox(box *box)
1136{
1137 // This could be heavily optimized, but let's keep things simple for now.
1138 // Optimized for sending less characters I mean, on slow serial links for instance.
1139
1140 char **bchars = (toys.optflags & FLAG_a) ? borderChars[0] : borderChars[1];
1141 char *left = "\0", *right = "\0";
1142 struct line *lines = NULL;
1143 int y = box->Y, current = (box == currentBox);
1144 uint16_t h = box->Y + box->H;
1145
1146 if (current)
1147 bchars = (toys.optflags & FLAG_a) ? borderCharsCurrent[0] : borderCharsCurrent[1];
1148
1149 // Slow and laborious way to figure out where in the linked list of lines we start from.
1150 // Wont scale well, but is simple.
1151 if (box->view && box->view->content)
1152 {
1153 int i = box->view->offsetY;
1154
1155 lines = &(box->view->content->lines);
1156 while (i--)
1157 {
1158 lines = lines->next;
1159 if (&(box->view->content->lines) == lines) // We are at the end if we have wrapped to the beginning.
1160 break;
1161 }
1162 }
1163
1164 if (box->flags & BOX_BORDER)
1165 {
1166 h--;
1167 left = right = bchars[1];
1168 drawLine(y++, box->X, box->X + box->W, bchars[2], bchars[0], NULL, bchars[3], current);
1169 }
1170
1171 while (y < h)
1172 {
1173 char *line = "";
1174
1175 if (lines)
1176 {
1177 lines = lines->next;
1178 if (&(box->view->content->lines) == lines) // We are at the end if we have wrapped to the beginning.
1179 lines = NULL;
1180 else
1181 line = lines->line;
1182 // Figure out which line is our current line while we are here.
1183 if (box->view->Y + (box->view->cY - box->view->offsetY) == y)
1184 box->view->line = lines;
1185 }
1186 drawContentLine(box->view, y++, box->X, box->X + box->W, left, " ", line, right, current);
1187 }
1188 if (box->flags & BOX_BORDER)
1189 drawLine(y++, box->X, box->X + box->W, bchars[4], bchars[0], NULL, bchars[5], current);
1190 fflush(stdout);
1191}
1192
1193void drawBoxes(box *box)
1194{
1195 if (box->sub1) // If there's one sub box, there's always two.
1196 {
1197 drawBoxes(box->sub1);
1198 drawBoxes(box->sub2);
1199 }
1200 else
1201 drawBox(box);
1202}
1203
1204void calcBoxes(box *box)
1205{
1206 if (box->sub1) // If there's one sub box, there's always two.
1207 {
1208 box->sub1->X = box->X;
1209 box->sub1->Y = box->Y;
1210 box->sub1->W = box->W;
1211 box->sub1->H = box->H;
1212 box->sub2->X = box->X;
1213 box->sub2->Y = box->Y;
1214 box->sub2->W = box->W;
1215 box->sub2->H = box->H;
1216 if (box->flags & BOX_HSPLIT)
1217 {
1218 box->sub1->H *= box->split;
1219 box->sub2->H -= box->sub1->H;
1220 box->sub2->Y += box->sub1->H;
1221 }
1222 else
1223 {
1224 box->sub1->W *= box->split;
1225 box->sub2->W -= box->sub1->W;
1226 box->sub2->X += box->sub1->W;
1227 }
1228 sizeViewToBox(box->sub1, -1, -1, -1, -1);
1229 calcBoxes(box->sub1);
1230 sizeViewToBox(box->sub2, -1, -1, -1, -1);
1231 calcBoxes(box->sub2);
1232 }
1233 // Move the cursor to where it is, to check it's not now outside the box.
1234 moveCursorAbsolute(box->view, box->view->cX, box->view->cY, 0, 0);
1235
1236 // We could call drawBoxes() here, but this is recursive, and so is drawBoxes().
1237 // The combination might be deadly. Drawing the content of a box might be an expensive operation.
1238 // Later we might use a dirty box flag to deal with this, if it's not too much of a complication.
1239}
1240
1241void deleteBox(view *view)
1242{
1243 box *box = view->box;
1244
1245 if (box->parent)
1246 {
1247 struct _box *oldBox = box, *otherBox = box->parent->sub1;
1248
1249 if (otherBox == oldBox)
1250 otherBox = box->parent->sub2;
1251 if (currentBox->parent == box->parent)
1252 currentBox = box->parent;
1253 box = box->parent;
1254 box->X = box->sub1->X;
1255 box->Y = box->sub1->Y;
1256 if (box->flags & BOX_HSPLIT)
1257 box->H = box->sub1->H + box->sub2->H;
1258 else
1259 box->W = box->sub1->W + box->sub2->W;
1260 box->flags &= ~BOX_HSPLIT;
1261 // Move the other sub boxes contents up to this box.
1262 box->sub1 = otherBox->sub1;
1263 box->sub2 = otherBox->sub2;
1264 if (box->sub1)
1265 {
1266 box->sub1->parent = box;
1267 box->sub2->parent = box;
1268 box->flags = otherBox->flags;
1269 if (currentBox == box)
1270 currentBox = box->sub1;
1271 }
1272 else
1273 {
1274 if (!box->parent)
1275 box->flags &= ~BOX_BORDER;
1276 box->split = 1.0;
1277 }
1278 otherBox->sub1 = NULL;
1279 otherBox->sub2 = NULL;
1280 // Safe to free the boxes now that we have all their contents.
1281 freeBox(otherBox);
1282 freeBox(oldBox);
1283 }
1284 // Otherwise it must be a single full screen box. Never delete that one, unless we are quitting.
1285
1286 // Start the recursive recalculation of all the sub boxes.
1287 calcBoxes(box);
1288 drawBoxes(box);
1289}
1290
1291void cloneBox(struct _box *box, struct _box *sub)
1292{
1293 sub->parent = box;
1294 // Only a full screen box has no border.
1295 sub->flags |= BOX_BORDER;
1296 sub->view = xmalloc(sizeof(struct _view));
1297 // TODO - After this is more stable, should check if the memcpy() is simpler than - xzalloc() then copy a few things manually.
1298 // Might even be able to arrange the structure so we can memcpy just part of it, leaving the rest blank.
1299 memcpy(sub->view, box->view, sizeof(struct _view));
1300 sub->view->damage = NULL;
1301 sub->view->data = NULL;
1302 sub->view->output = NULL;
1303 sub->view->box = sub;
1304 if (box->view->prompt)
1305 sub->view->prompt = strdup(box->view->prompt);
1306}
1307
1308void splitBox(box *box, float split)
1309{
1310 uint16_t size;
1311 int otherBox = 0;
1312
1313 // First some sanity checks.
1314 if (0.0 > split)
1315 {
1316 // TODO - put this in the status line, or just silently fail. Also, better message. lol
1317 fprintf(stderr, "User is crazy.\n");
1318 return;
1319 }
1320 else if (1.0 <= split) // User meant to unsplit, and it may already be split.
1321 {
1322 // Actually, this means that the OTHER sub box gets deleted.
1323 if (box->parent)
1324 {
1325 if (box == box->parent->sub1)
1326 deleteBox(box->parent->sub2->view);
1327 else
1328 deleteBox(box->parent->sub1->view);
1329 }
1330 return;
1331 }
1332 else if (0.0 < split) // This is the normal case, so do nothing.
1333 {
1334 }
1335 else // User meant to delete this, zero split.
1336 {
1337 deleteBox(box->view);
1338 return;
1339 }
1340 if (box->flags & BOX_HSPLIT)
1341 size = box->H;
1342 else
1343 size = box->W;
1344 if (6 > size) // Is there room for 2 borders for each sub box and one character of content each?
1345 // TODO - also should check the contents minimum size.
1346 // No need to check the no border case, that's only for full screen.
1347 // People using terminals smaller than 6 characters get what they deserve.
1348 {
1349 // TODO - put this in the status line, or just silently fail.
1350 fprintf(stderr, "Box is too small to split.\n");
1351 return;
1352 }
1353
1354 // Note that a split box is actually three boxes. The parent, which is not drawn, and the two subs, which are.
1355 // Based on the assumption that there wont be lots of boxes, this keeps things simple.
1356 // It's possible that the box has already been split, and this is called just to update the split.
1357 box->split = split;
1358 if (NULL == box->sub1) // If not split already, do so.
1359 {
1360 box->sub1 = xzalloc(sizeof(struct _box));
1361 box->sub2 = xzalloc(sizeof(struct _box));
1362 cloneBox(box, box->sub1);
1363 cloneBox(box, box->sub2);
1364 if (box->flags & BOX_HSPLIT)
1365 {
1366 // Split the boxes in the middle of the content.
1367 box->sub2->view->offsetY += (box->H * box->split) - 2;
1368 // Figure out which sub box the cursor will be in, then update the cursor in the other box.
1369 if (box->sub1->view->cY < box->sub2->view->offsetY)
1370 box->sub2->view->cY = box->sub2->view->offsetY;
1371 else
1372 {
1373 box->sub1->view->cY = box->sub2->view->offsetY - 1;
1374 otherBox = 1;
1375 }
1376 }
1377 else
1378 {
1379 // Split the boxes in the middle of the content.
1380 box->sub2->view->offsetX += (box->W * box->split) - 2;
1381 // Figure out which sub box the cursor will be in, then update the cursor in the other box.
1382 if (box->sub1->view->cX < box->sub2->view->offsetX)
1383 box->sub2->view->cX = box->sub2->view->offsetX;
1384 else
1385 {
1386 box->sub1->view->cX = box->sub2->view->offsetX - 1;
1387 otherBox = 1;
1388 }
1389 }
1390 }
1391
1392 if ((currentBox == box) && (box->sub1))
1393 {
1394 if (otherBox)
1395 currentBox = box->sub2;
1396 else
1397 currentBox = box->sub1;
1398 }
1399
1400 // Start the recursive recalculation of all the sub boxes.
1401 calcBoxes(box);
1402 drawBoxes(box);
1403}
1404
1405// TODO - Might be better to just have a double linked list of boxes, and traverse that instead.
1406// Except that leaves a problem when deleting boxes, could end up with a blank space.
1407void switchBoxes(view *view)
1408{
1409 box *box = view->box;
1410
1411 // The assumption here is that box == currentBox.
1412 struct _box *oldBox = currentBox;
1413 struct _box *thisBox = box;
1414 int backingUp = 0;
1415
1416 // Depth first traversal.
1417 while ((oldBox == currentBox) && (thisBox->parent))
1418 {
1419 if (thisBox == thisBox->parent->sub1)
1420 {
1421 if (backingUp && (thisBox->parent))
1422 currentBox = thisBox->parent->sub2;
1423 else if (thisBox->sub1)
1424 currentBox = thisBox->sub1;
1425 else
1426 currentBox = thisBox->parent->sub2;
1427 }
1428 else if (thisBox == thisBox->parent->sub2)
1429 {
1430 thisBox = thisBox->parent;
1431 backingUp = 1;
1432 }
1433 }
1434
1435 // If we have not found the next box to move to, move back to the beginning.
1436 if (oldBox == currentBox)
1437 currentBox = rootBox;
1438
1439 // If we ended up on a parent box, go to it's first sub.
1440 while (currentBox->sub1)
1441 currentBox = currentBox->sub1;
1442
1443 // Just redraw them all.
1444 drawBoxes(rootBox);
1445}
1446
1447// TODO - It might be better to do away with this bunch of single line functions
1448// and map script commands directly to lower functions.
1449// How to deal with the various argument needs?
1450// Might be where we can re use the toybox argument stuff.
1451
1452void halveBoxHorizontally(view *view)
1453{
1454 view->box->flags |= BOX_HSPLIT;
1455 splitBox(view->box, 0.5);
1456}
1457
1458void halveBoxVertically(view *view)
1459{
1460 view->box->flags &= ~BOX_HSPLIT;
1461 splitBox(view->box, 0.5);
1462}
1463
1464void switchMode(view *view)
1465{
1466 currentBox->view->mode++;
1467 // Assumes that modes will always have a key mapping, which I think is a safe bet.
1468 if (NULL == currentBox->view->content->context->modes[currentBox->view->mode].keys)
1469 currentBox->view->mode = 0;
1470 commandMode = currentBox->view->content->context->modes[currentBox->view->mode].flags & 1;
1471}
1472
1473void leftChar(view *view)
1474{
1475 moveCursorRelative(view, -1, 0, 0, 0);
1476}
1477
1478void rightChar(view *view)
1479{
1480 moveCursorRelative(view, 1, 0, 0, 0);
1481}
1482
1483void upLine(view *view)
1484{
1485 moveCursorRelative(view, 0, -1, 0, 0);
1486}
1487
1488void downLine(view *view)
1489{
1490 moveCursorRelative(view, 0, 1, 0, 0);
1491}
1492
1493void upPage(view *view)
1494{
1495 moveCursorRelative(view, 0, 0 - (view->H - 1), 0, 0 - (view->H - 1));
1496}
1497
1498void downPage(view *view)
1499{
1500 moveCursorRelative(view, 0, view->H - 1, 0, view->H - 1);
1501}
1502
1503void endOfLine(view *view)
1504{
1505 moveCursorAbsolute(view, strlen(view->prompt) + view->oW, view->cY, 0, 0);
1506}
1507
1508void startOfLine(view *view)
1509{
1510 // TODO - add the advanced editing "smart home".
1511 moveCursorAbsolute(view, strlen(view->prompt), view->cY, 0, 0);
1512}
1513
1514void splitLine(view *view)
1515{
1516 // TODO - should move this into mooshLines().
1517 addLine(view->content, view->line, &(view->line->line[view->iX]), 0);
1518 view->line->line[view->iX] = '\0';
1519 moveCursorAbsolute(view, 0, view->cY + 1, 0, 0);
1520 if (view->box)
1521 drawBox(view->box);
1522}
1523
1524void deleteChar(view *view)
1525{
1526 // TODO - should move this into mooshLines().
1527 // If we are at the end of the line, then join this and the next line.
1528 if (view->oW == view->cX)
1529 {
1530 // Only if there IS a next line.
1531 if (&(view->content->lines) != view->line->next)
1532 {
1533 mooshStrings(view->line, view->line->next->line, view->iX, 1, !overWriteMode);
1534 view->line->next->line = NULL;
1535 freeLine(view->content, view->line->next);
1536 // TODO - should check if we are on the last page, then deal with scrolling.
1537 if (view->box)
1538 drawBox(view->box);
1539 }
1540 }
1541 else
1542 mooshStrings(view->line, NULL, view->iX, 1, !overWriteMode);
1543}
1544
1545void backSpaceChar(view *view)
1546{
1547 if (moveCursorRelative(view, -1, 0, 0, 0))
1548 deleteChar(view);
1549}
1550
1551void saveContent(view *view)
1552{
1553 saveFile(view->content);
1554}
1555
1556void executeLine(view *view)
1557{
1558 struct line *result = view->line;
1559
1560 // Don't bother doing much if there's nothing on this line.
1561 if (result->line[0])
1562 {
1563 doCommand(currentBox->view, result->line);
1564 // If we are not at the end of the history contents.
1565 if (&(view->content->lines) != result->next)
1566 {
1567 struct line *line = view->content->lines.prev;
1568
1569 // Remove the line first.
1570 result->next->prev = result->prev;
1571 result->prev->next = result->next;
1572 // Check if the last line is already blank, then remove it.
1573 if ('\0' == line->line[0])
1574 {
1575 freeLine(view->content, line);
1576 line = view->content->lines.prev;
1577 }
1578 // Then add it to the end.
1579 result->next = line->next;
1580 result->prev = line;
1581 line->next->prev = result;
1582 line->next = result;
1583 view->cY = view->content->lines.length - 1;
1584 }
1585 moveCursorAbsolute(view, 0, view->content->lines.length, 0, 0);
1586 // Make sure there is one blank line at the end.
1587 if ('\0' != view->line->line[0])
1588 {
1589 endOfLine(view);
1590 splitLine(view);
1591 }
1592 }
1593
1594 saveFile(view->content);
1595}
1596
1597void quit(view *view)
1598{
1599 handle_keys_quit();
1600}
1601
1602void nop(view *view)
1603{
1604 // 'tis a nop, don't actually do anything.
1605}
1606
1607
1608typedef void (*CSIhandler) (long extra, int *code, int count);
1609
1610struct CSI
1611{
1612 char *code;
1613 CSIhandler func;
1614};
1615
1616static void termSize(long extra, int *params, int count)
1617{
1618 struct _view *view = (struct _view *) extra; // Though we pretty much stomp on this straight away.
1619 int r = params[0], c = params[1];
1620
1621 // The defaults are 1, which get ignored by the heuristic below.
1622 // Check it's not an F3 key variation, coz some of them use the same CSI function code.
1623 // This is a heuristic, we are checking against an unusable terminal size.
1624 // TODO - Double check what the maximum F3 variations can be.
1625 if ((2 == count) && (8 < r) && (8 < c))
1626 {
1627 commandLine->Y = r;
1628 commandLine->W = c;
1629 rootBox->W = c;
1630 rootBox->H = r - 1;
1631 sizeViewToBox(rootBox, -1, -1, -1, -1);
1632 calcBoxes(rootBox);
1633 drawBoxes(rootBox);
1634
1635 // Move the cursor to where it is, to check it's not now outside the terminal window.
1636 moveCursorAbsolute(rootBox->view, rootBox->view->cX, rootBox->view->cY, 0, 0);
1637
1638 // We have no idea which is the current view now.
1639 if (commandMode) view = commandLine;
1640 else view = currentBox->view;
1641 updateLine(view);
1642 }
1643}
1644
1645struct CSI CSIcommands[] =
1646{
1647 {"R", termSize} // Parameters are cursor line and column. Note this may be sent at other times, not just during terminal resize.
1648};
1649
1650
1651// Callback for incoming sequences from the terminal.
1652static int handleEvent(long extra, struct keyevent *event)
1653{
1654 switch (event->type)
1655 {
1656 case HK_CSI :
1657 {
1658 int j;
1659
1660 for (j = 0; j < ARRAY_LEN(CSIcommands); j++)
1661 {
1662 if (strcmp(CSIcommands[j].code, event->sequence) == 0)
1663 {
1664 CSIcommands[j].func(extra, event->params, event->count);
1665 break;
1666 }
1667 }
1668 break;
1669 }
1670
1671 case HK_KEYS :
1672 {
1673 struct _view *view = (struct _view *) extra; // Though we pretty much stomp on this straight away.
1674 struct keyCommand *commands = currentBox->view->content->context->modes[currentBox->view->mode].keys;
1675 int j, l = strlen(event->sequence);
1676
1677 // Coz things might change out from under us, find the current view.
1678 if (commandMode) view = commandLine;
1679 else view = currentBox->view;
1680
1681 // Search for a key sequence bound to a command.
1682 for (j = 0; commands[j].key; j++)
1683 {
1684 if (strncmp(commands[j].key, event->sequence, l) == 0)
1685 {
1686 // If it's a partial match, keep accumulating them.
1687 if (strlen(commands[j].key) != l)
1688 return 0;
1689 else
1690 {
1691 doCommand(view, commands[j].command);
1692 return 1;
1693 }
1694 }
1695 }
1696
1697 // See if it's ordinary keys.
1698 // NOTE - with vi style ordinary keys can be commands,
1699 // but they would be found by the command check above first.
1700 if (!event->isTranslated)
1701 {
1702 // TODO - Should check for tabs to, and insert them.
1703 // Though better off having a function for that?
1704 mooshStrings(view->line, event->sequence, view->iX, 0, !overWriteMode);
1705 view->oW = formatLine(view, view->line->line, &(view->output));
1706 moveCursorRelative(view, strlen(event->sequence), 0, 0, 0);
1707 updateLine(view);
1708 }
1709 break;
1710 }
1711
1712 default : break;
1713 }
1714
1715 // Tell handle_keys to drop it, coz we dealt with it, or it's not one of ours.
1716 return 1;
1717}
1718
1719
1720// The default command to function mappings, with help text. Any editor that does not have it's own commands can use these for keystroke binding and such.
1721// Though most of the editors have their own variation.
1722// TODO - Maybe just use the joe one as default, it uses short names at least.
1723// Though vi is the only one in POSIX, so might be better to treat that one as the "standard" default.
1724// With some commands from others for stuff vi doesn't support.
1725struct function simpleEditCommands[] =
1726{
1727 {"backSpaceChar", "Back space last character.", 0, {backSpaceChar}},
1728 {"deleteBox", "Delete a box.", 0, {deleteBox}},
1729 {"deleteChar", "Delete current character.", 0, {deleteChar}},
1730 {"downLine", "Move cursor down one line.", 0, {downLine}},
1731 {"downPage", "Move cursor down one page.", 0, {downPage}},
1732 {"endOfLine", "Go to end of line.", 0, {endOfLine}},
1733 {"executeLine", "Execute a line as a script.", 0, {executeLine}},
1734 {"leftChar", "Move cursor left one character.", 0, {leftChar}},
1735 {"quit", "Quit the application.", 0, {quit}},
1736 {"rightChar", "Move cursor right one character.", 0, {rightChar}},
1737 {"save", "Save.", 0, {saveContent}},
1738 {"splitH", "Split box in half horizontally.", 0, {halveBoxHorizontally}},
1739 {"splitLine", "Split line at cursor.", 0, {splitLine}},
1740 {"splitV", "Split box in half vertically.", 0, {halveBoxVertically}},
1741 {"startOfLine", "Go to start of line.", 0, {startOfLine}},
1742 {"switchBoxes", "Switch to another box.", 0, {switchBoxes}},
1743 {"switchMode", "Switch between command and box.", 0, {switchMode}},
1744 {"upLine", "Move cursor up one line.", 0, {upLine}},
1745 {"upPage", "Move cursor up one page.", 0, {upPage}},
1746 {NULL, NULL, 0, {NULL}}
1747};
1748
1749// Construct a simple command line.
1750
1751// The key to command mappings.
1752// TODO - Should not move off the ends of the line to the next / previous line.
1753struct keyCommand simpleCommandKeys[] =
1754{
1755 {"BS", "backSpaceChar"},
1756 {"Del", "deleteChar"},
1757 {"Down", "downLine"},
1758 {"End", "endOfLine"},
1759 {"F10", "quit"},
1760 {"Home", "startOfLine"},
1761 {"Left", "leftChar"},
1762 {"Enter", "executeLine"},
1763 {"Return", "executeLine"},
1764 {"Right", "rightChar"},
1765 {"Esc", "switchMode"},
1766 {"Up", "upLine"},
1767 {NULL, NULL}
1768};
1769
1770
1771// Construct a simple emacs editor.
1772
1773// Mostly control keys, some meta keys.
1774// Ctrl-h and Ctrl-x have more keys in the commands. Some of those extra keys are commands by themselves. Up to "Ctrl-x 4 Ctrl-g" which apparently is identical to just Ctrl-g. shrugs
1775// Ctrl-h is backspace / del. Pffft.
1776// Meta key is either Alt-keystroke, Esc keystroke, or an actual Meta-keystroke (do they still exist?).
1777// TODO - Alt and Meta not supported yet, so using Esc.
1778// Windows commands.
1779
1780// readline uses these same commands, and defaults to emacs keystrokes.
1781struct function simpleEmacsCommands[] =
1782{
1783 {"delete-backward-char", "Back space last character.", 0, {backSpaceChar}},
1784 {"delete-window", "Delete a box.", 0, {deleteBox}},
1785 {"delete-char", "Delete current character.", 0, {deleteChar}},
1786 {"next-line", "Move cursor down one line.", 0, {downLine}},
1787 {"scroll-up", "Move cursor down one page.", 0, {downPage}},
1788 {"end-of-line", "Go to end of line.", 0, {endOfLine}},
1789 {"accept-line", "Execute a line as a script.", 0, {executeLine}}, // From readline, which uses emacs commands, coz mg at least does not seem to have this.
1790 {"backward-char", "Move cursor left one character.", 0, {leftChar}},
1791 {"save-buffers-kill-emacs", "Quit the application.", 0, {quit}}, // TODO - Does more than just quit.
1792 {"forward-char", "Move cursor right one character.", 0, {rightChar}},
1793 {"save-buffer", "Save.", 0, {saveContent}},
1794 {"split-window-horizontally", "Split box in half horizontally.", 0, {halveBoxHorizontally}}, // TODO - Making this one up for now, mg does not have it.
1795 {"newline", "Split line at cursor.", 0, {splitLine}},
1796 {"split-window-vertically", "Split box in half vertically.", 0, {halveBoxVertically}},
1797 {"beginning-of-line", "Go to start of line.", 0, {startOfLine}},
1798 {"other-window", "Switch to another box.", 0, {switchBoxes}}, // There is also "previous-window" for going in the other direction, which we don't support yet.
1799 {"execute-extended-command", "Switch between command and box.", 0, {switchMode}}, // Actually a one time invocation of the command line.
1800 {"previous-line", "Move cursor up one line.", 0, {upLine}},
1801 {"scroll-down", "Move cursor up one page.", 0, {upPage}},
1802 {NULL, NULL, 0, {NULL}}
1803};
1804
1805// The key to command mappings.
1806struct keyCommand simpleEmacsKeys[] =
1807{
1808 {"BS", "delete-backward-char"},
1809 {"Del", "delete-backward-char"},
1810 {"^D", "delete-char"},
1811 {"Down", "next-line"},
1812 {"^N", "next-line"},
1813 {"End", "end-of-line"},
1814 {"^E", "end-of-line"},
1815 {"^X^C", "save-buffers-kill-emacs"},
1816 {"^X^S", "save-buffer"},
1817 {"Home", "beginning-of-line"},
1818 {"^A", "beginning-of-line"},
1819 {"Left", "backward-char"},
1820 {"^B", "backward-char"},
1821 {"PgDn", "scroll-up"},
1822 {"^V", "scroll-up"},
1823 {"PgUp", "scroll-down"},
1824 {"Escv", "scroll-down"}, // M-v
1825 {"Enter", "newline"},
1826 {"Return", "newline"},
1827 {"Right", "forward-char"},
1828 {"^F", "forward-char"},
1829 {"Escx", "execute-extended-command"}, // M-x
1830 {"^X2", "split-window-vertically"},
1831 {"^XP", "other-window"},
1832 {"^X0", "delete-window"},
1833 {"Up", "previous-line"},
1834 {"^P", "previous-line"},
1835 {NULL, NULL}
1836};
1837
1838struct keyCommand simpleEmacsCommandKeys[] =
1839{
1840 {"Del", "delete-backwards-char"},
1841 {"^D", "delete-char"},
1842 {"Down", "next-line"},
1843 {"^N", "next-line"},
1844 {"End", "end-of-line"},
1845 {"^E", "end-of-line"},
1846 {"Home", "beginning-of-line"},
1847 {"^A", "beginning-of-line"},
1848 {"Left", "backward-char"},
1849 {"^B", "backward-char"},
1850 {"Right", "forward-char"},
1851 {"^F", "forward-char"},
1852 {"Up", "previous-line"},
1853 {"^P", "previous-line"},
1854 {"Enter", "accept-line"},
1855 {"Return", "accept-line"},
1856 {"Escx", "execute-extended-command"},
1857 {NULL, NULL}
1858};
1859
1860// An array of various modes.
1861struct mode simpleEmacsMode[] =
1862{
1863 {simpleEmacsKeys, NULL, NULL, 0},
1864 {simpleEmacsCommandKeys, NULL, NULL, 1},
1865 {NULL, NULL, NULL}
1866};
1867
1868// Put it all together into a simple editor context.
1869struct context simpleEmacs =
1870{
1871 simpleEmacsCommands,
1872 simpleEmacsMode,
1873 NULL,
1874 NULL,
1875 NULL
1876};
1877
1878
1879// Construct a simple joe / wordstar editor, using joe is the reference, seems to be the popular Unix variant.
1880// Esc x starts up the command line.
1881// Has multi control key combos. Mostly Ctrl-K, Ctrl-[ (Esc), (Ctrl-B, Ctrl-Q in wordstar and delphi), but might be others.
1882// Can't find a single list of command mappings for joe, gotta search all over. sigh
1883// Even the command line keystroke I stumbled on (Esc x) is not documented.
1884// Note that you don't have to let go of the Ctrl key for the second keystroke, but you can.
1885
1886// From http://joe-editor.sourceforge.net/list.html
1887// TODO - Some of these might be wrong. Just going by the inadequate joe docs for now.
1888struct function simpleJoeCommands[] =
1889{
1890 {"backs", "Back space last character.", 0, {backSpaceChar}},
1891 {"abort", "Delete a box.", 0, {deleteBox}}, // TODO - Should do quit if it's the last window.
1892 {"delch", "Delete current character.", 0, {deleteChar}},
1893 {"dnarw", "Move cursor down one line.", 0, {downLine}},
1894 {"pgdn", "Move cursor down one page.", 0, {downPage}},
1895 {"eol", "Go to end of line.", 0, {endOfLine}},
1896 {"ltarw", "Move cursor left one character.", 0, {leftChar}},
1897 {"killjoe", "Quit the application.", 0, {quit}},
1898 {"rtarw", "Move cursor right one character.", 0, {rightChar}},
1899 {"save", "Save.", 0, {saveContent}},
1900 {"splitw", "Split box in half horizontally.", 0, {halveBoxHorizontally}},
1901 {"open", "Split line at cursor.", 0, {splitLine}},
1902 {"bol", "Go to start of line.", 0, {startOfLine}},
1903 {"home", "Go to start of line.", 0, {startOfLine}},
1904 {"nextw", "Switch to another box.", 0, {switchBoxes}}, // This is "next window", there's also "previous window" which we don't support yet.
1905 {"execmd", "Switch between command and box.", 0, {switchMode}}, // Actually I think this just switches to the command mode, not back and forth. Or it might execute the actual command.
1906 {"uparw", "Move cursor up one line.", 0, {upLine}},
1907 {"pgup", "Move cursor up one page.", 0, {upPage}},
1908
1909 // Not an actual joe command.
1910 {"executeLine", "Execute a line as a script.", 0, {executeLine}}, // Perhaps this should be execmd?
1911 {NULL, NULL, 0, {NULL}}
1912};
1913
1914struct keyCommand simpleJoeKeys[] =
1915{
1916 {"BS", "backs"},
1917 {"^D", "delch"},
1918 {"Down", "dnarw"},
1919 {"^N", "dnarw"},
1920 {"^E", "eol"},
1921 {"^C", "killjoe"},
1922 {"^Kd", "save"},
1923 {"^K^D" "save"},
1924 {"^A", "bol"},
1925 {"Left", "ltarw"},
1926 {"^B", "ltarw"},
1927 {"^V", "pgdn"}, // Actually half a page.
1928 {"^U", "pgup"}, // Actually half a page.
1929 {"Enter", "open"},
1930 {"Return", "open"},
1931 {"Right", "rtarw"},
1932 {"^F", "rtarw"},
1933 {"Escx", "execmd"},
1934 {"Esc^X", "execmd"},
1935 {"^Ko", "splitw"},
1936 {"^K^O", "splitw"},
1937 {"^Kn", "nextw"},
1938 {"^K^N", "nextw"},
1939 {"^Kx", "killjoe"}, // TODO - Should ask if it should save if it's been modified. A good generic thing to do anyway.
1940 {"^K^X", "abort"}, // TODO - These two both close a window, and quit if that was the last window.
1941 {"Up", "uparw"},
1942 {"^P", "uparw"},
1943 {NULL, NULL}
1944};
1945
1946struct keyCommand simpleJoeCommandKeys[] =
1947{
1948 {"BS", "backs"},
1949 {"^D", "delch"},
1950 {"Down", "dnarw"},
1951 {"^N", "dnarw"},
1952 {"^E", "eol"},
1953 {"^A", "bol"},
1954 {"Left", "ltarw"},
1955 {"^B", "ltarw"},
1956 {"Right", "rtarw"},
1957 {"^F", "rtarw"},
1958 {"Escx", "execmd"},
1959 {"Esc^X", "execmd"},
1960 {"Up", "uparw"},
1961 {"^P", "uparw"},
1962 {"Enter", "executeLine"},
1963 {"Return", "executeLine"},
1964 {NULL, NULL}
1965};
1966
1967struct mode simpleJoeMode[] =
1968{
1969 {simpleJoeKeys, NULL, NULL, 0},
1970 {simpleJoeCommandKeys, NULL, NULL, 1},
1971 {NULL, NULL, NULL, 0}
1972};
1973
1974struct context simpleJoe =
1975{
1976 simpleJoeCommands,
1977 simpleJoeMode,
1978 NULL,
1979 NULL,
1980 NULL
1981};
1982
1983
1984// Simple more and / or less.
1985// '/' and '?' for search command mode. I think they both have some ex commands with the usual : command mode starter.
1986// No cursor movement, just scrolling.
1987// TODO - Put content into read only mode.
1988// TODO - actually implement read only mode where up and down one line do actual scrolling instead of cursor movement.
1989
1990struct keyCommand simpleLessKeys[] =
1991{
1992 {"Down", "downLine"},
1993 {"j", "downLine"},
1994 {"Enter", "downLine"},
1995 {"Return", "downLine"},
1996 {"End", "endOfLine"},
1997 {"q", "quit"},
1998 {":q", "quit"}, // TODO - A vi ism, should do ex command stuff instead.
1999 {"ZZ", "quit"},
2000 {"PgDn", "downPage"},
2001 {"f", "downPage"},
2002 {" ", "downPage"},
2003 {"^F", "downPage"},
2004 {"Left", "leftChar"},
2005 {"Right", "rightChar"},
2006 {"PgUp", "upPage"},
2007 {"b", "upPage"},
2008 {"^B", "upPage"},
2009 {"Up", "upLine"},
2010 {"k", "upLine"},
2011 {NULL, NULL}
2012};
2013
2014struct mode simpleLessMode[] =
2015{
2016 {simpleLessKeys, NULL, NULL, 0},
2017 {simpleCommandKeys, NULL, NULL, 1},
2018 {NULL, NULL, NULL}
2019};
2020
2021struct context simpleLess =
2022{
2023 simpleEditCommands,
2024 simpleLessMode,
2025 NULL,
2026 NULL,
2027 NULL
2028};
2029
2030struct keyCommand simpleMoreKeys[] =
2031{
2032 {"j", "downLine"},
2033 {"Enter", "downLine"},
2034 {"Return", "downLine"},
2035 {"q", "quit"},
2036 {":q", "quit"}, // See comments for "less".
2037 {"ZZ", "quit"},
2038 {"f", "downPage"},
2039 {" ", "downPage"},
2040 {"^F", "downPage"},
2041 {"b", "upPage"},
2042 {"^B", "upPage"},
2043 {"k", "upLine"},
2044 {NULL, NULL}
2045};
2046
2047struct mode simpleMoreMode[] =
2048{
2049 {simpleMoreKeys, NULL, NULL, 0},
2050 {simpleCommandKeys, NULL, NULL, 1},
2051 {NULL, NULL, NULL}
2052};
2053
2054struct context simpleMore =
2055{
2056 simpleEditCommands,
2057 simpleMoreMode,
2058 NULL,
2059 NULL,
2060 NULL
2061};
2062
2063
2064// Construct a simple mcedit / cool edit editor.
2065
2066struct keyCommand simpleMceditKeys[] =
2067{
2068 {"BS", "backSpaceChar"},
2069 {"Del", "deleteChar"},
2070 {"Down", "downLine"},
2071 {"End", "endOfLine"},
2072 {"F10", "quit"},
2073 {"Esc0", "quit"},
2074 {"F2", "save"},
2075 {"Esc2", "save"},
2076 {"Home", "startOfLine"},
2077 {"Left", "leftChar"},
2078 {"PgDn", "downPage"},
2079 {"PgUp", "upPage"},
2080 {"Enter", "splitLine"},
2081 {"Return", "splitLine"},
2082 {"Right", "rightChar"},
2083 {"Shift F2", "switchMode"}, // MC doesn't have a command mode.
2084 {"Esc:", "switchMode"}, // Sorta vi like, and coz tmux is screwing with the shift function keys somehow.
2085 {"Esc|", "splitV"}, // MC doesn't have a split window concept, so make these up to match tmux more or less.
2086 {"Esc-", "splitH"},
2087 {"Esco", "switchBoxes"},
2088 {"Escx", "deleteBox"},
2089 {"Up", "upLine"},
2090{"^C", "switchMode"}, // To test the signal stopper.
2091{"^D", "switchMode"}, // To test the signal stopper.
2092{"^Q", "switchMode"}, // To test the signal stopper.
2093{"^S", "switchMode"}, // To test the signal stopper.
2094{"^T", "switchMode"}, // To test the signal stopper.
2095{"^Z", "switchMode"}, // To test the signal stopper.
2096{"^\\", "switchMode"}, // To test the signal stopper.
2097{"F1", "switchMode"}, // To test various function keys.
2098{"F3", "switchMode"}, // To test various function keys.
2099{"F4", "switchMode"}, // To test various function keys.
2100{"F5", "switchMode"}, // To test various function keys.
2101{"F6", "switchMode"}, // To test various function keys.
2102{"F7", "switchMode"}, // To test various function keys.
2103{"F8", "switchMode"}, // To test various function keys.
2104{"F9", "switchMode"}, // To test various function keys.
2105 {NULL, NULL}
2106};
2107
2108struct mode simpleMceditMode[] =
2109{
2110 {simpleMceditKeys, NULL, NULL, 0},
2111 {simpleCommandKeys, NULL, NULL, 1},
2112 {NULL, NULL, NULL}
2113};
2114
2115struct context simpleMcedit =
2116{
2117 simpleEditCommands,
2118 simpleMceditMode,
2119 NULL,
2120 NULL,
2121 NULL
2122};
2123
2124
2125// Simple nano editor.
2126// Has key to function bindings, but no command line mode. Has "enter parameter on this line" mode for some commands.
2127// Control and meta keys, only singles, unlike emacs and joe.
2128// Can have multiple buffers, but no windows. Think I can skip that for simple editor.
2129
2130struct function simpleNanoCommands[] =
2131{
2132 {"backSpaceChar", "Back space last character.", 0, {backSpaceChar}},
2133 {"delete", "Delete current character.", 0, {deleteChar}},
2134 {"down", "Move cursor down one line.", 0, {downLine}},
2135 {"downPage", "Move cursor down one page.", 0, {downPage}},
2136 {"end", "Go to end of line.", 0, {endOfLine}},
2137 {"left", "Move cursor left one character.", 0, {leftChar}},
2138 {"exit", "Quit the application.", 0, {quit}},
2139 {"right", "Move cursor right one character.", 0, {rightChar}},
2140 {"writeout", "Save.", 0, {saveContent}},
2141 {"enter", "Split line at cursor.", 0, {splitLine}},
2142 {"home", "Go to start of line.", 0, {startOfLine}},
2143 {"up", "Move cursor up one line.", 0, {upLine}},
2144 {"upPage", "Move cursor up one page.", 0, {upPage}},
2145 {NULL, NULL, 0, {NULL}}
2146};
2147
2148
2149// Hmm, back space, page up, and page down don't seem to have bindable commands according to the web page, but they are bound to keys anyway.
2150struct keyCommand simpleNanoKeys[] =
2151{
2152// TODO - Delete key is ^H dammit. Find the alternate Esc sequence for Del.
2153// {"^H", "backSpaceChar"}, // ?
2154 {"BS", "backSpaceChar"},
2155 {"^D", "delete"},
2156 {"Del", "delete"},
2157 {"^N", "down"},
2158 {"Down", "down"},
2159 {"^E", "end"},
2160 {"End", "end"},
2161 {"^X", "exit"},
2162 {"F2", "exit"},
2163 {"^O", "writeout"},
2164 {"F3", "writeout"},
2165 {"^A", "home"},
2166 {"Home", "home"},
2167 {"^B", "left"},
2168 {"Left", "left"},
2169 {"^V", "downPage"}, // ?
2170 {"PgDn", "downPage"},
2171 {"^Y", "upPage"}, // ?
2172 {"PgUp", "upPage"},
2173 {"Enter", "enter"}, // TODO - Not sure if this is correct.
2174 {"Return", "enter"}, // TODO - Not sure if this is correct.
2175 {"^F", "right"},
2176 {"Right", "right"},
2177 {"^P", "up"},
2178 {"Up", "up"},
2179 {NULL, NULL}
2180};
2181
2182struct mode simpleNanoMode[] =
2183{
2184 {simpleNanoKeys, NULL, NULL, 0},
2185 {NULL, NULL, NULL}
2186};
2187
2188struct context simpleNano =
2189{
2190 simpleNanoCommands,
2191 simpleNanoMode,
2192 NULL,
2193 NULL,
2194 NULL
2195};
2196
2197
2198// Construct a simple vi editor.
2199// Only vi is not so simple. lol
2200// The "command line" modes are /, ?, :, and !,
2201// / is regex search.
2202// ? is regex search backwards.
2203// : is ex command mode.
2204// ! is replace text with output from shell command mode.
2205// Arrow keys do the right thing in "normal" mode, but not in insert mode.
2206// "i" goes into insert mode, "Esc" (or Ctrl-[ or Ctrl-C) gets you out. So much for "you can do it all touch typing on the home row". Pffft
2207// Ah, the Esc key WAS a lot closer to the home row (where Tab usually is now) on the original keyboard vi was designed for, ADM3A.
2208// Which is also the keyboard with the arrow keys marked on h, j, k, and l keys.
2209// Did I mention that vi is just a horrid historic relic that should have died long ago?
2210// Emacs looks to have the same problem, originally designed for an ancient keyboard that is nothing like what people actually use these days.
2211// "h", "j", "k", "l" move cursor, which is just random keys for dvorak users.
2212// ":" goes into ex command mode.
2213// ":q" deletes current window in vim.
2214// ":qa!" goes into ex mode and does some sort of quit command.
2215// The 'q' is short for quit, the ! is an optional argument to quit. No idea yet what the a is for, all windows?
2216// Del or "x" to delete a character. Del in insert mode.
2217// "X" to backspace. BS or Ctrl-H to backspace in insert mode.
2218// NOTE - Backspace in normal mode just moves left.
2219// Tab or Ctrl-I to insert a tab in insert mode.
2220// Return in normal mode goes to the start of the next line, or splits the line in insert mode.
2221// ":help" opens a window with help text.
2222// Vim window commands.
2223
2224// Vi needs extra variables and functions.
2225static int viTempExMode;
2226
2227void viMode(view *view)
2228{
2229 currentBox->view->mode = 0;
2230 commandMode = 0;
2231 viTempExMode = 0;
2232}
2233
2234void viInsertMode(view *view)
2235{
2236 currentBox->view->mode = 1;
2237 commandMode = 0;
2238}
2239
2240void viExMode(view *view)
2241{
2242 currentBox->view->mode = 2;
2243 commandMode = 1;
2244 // TODO - Should change this based on the event, : or Q.
2245 viTempExMode = 1;
2246 commandLine->prompt = xrealloc(commandLine->prompt, 2);
2247 strcpy(commandLine->prompt, ":");
2248}
2249
2250void viBackSpaceChar(view *view)
2251{
2252 if ((2 == currentBox->view->mode) && (0 == view->cX) && viTempExMode)
2253 viMode(view);
2254 else
2255 backSpaceChar(view);
2256}
2257
2258void viStartOfNextLine(view *view)
2259{
2260 startOfLine(view);
2261 downLine(view);
2262}
2263
2264struct function simpleViCommands[] =
2265{
2266 // These are actual ex commands.
2267 {"insert", "Switch to insert mode.", 0, {viInsertMode}},
2268 {"quit", "Quit the application.", 0, {quit}},
2269 {"visual", "Switch to visual mode.", 0, {viMode}},
2270 {"write", "Save.", 0, {saveContent}},
2271
2272 // These are not ex commands.
2273 {"backSpaceChar", "Back space last character.", 0, {viBackSpaceChar}},
2274 {"deleteBox", "Delete a box.", 0, {deleteBox}},
2275 {"deleteChar", "Delete current character.", 0, {deleteChar}},
2276 {"downLine", "Move cursor down one line.", 0, {downLine}},
2277 {"downPage", "Move cursor down one page.", 0, {downPage}},
2278 {"endOfLine", "Go to end of line.", 0, {endOfLine}},
2279 {"executeLine", "Execute a line as a script.", 0, {executeLine}},
2280 {"exMode", "Switch to ex mode.", 0, {viExMode}},
2281 {"leftChar", "Move cursor left one character.", 0, {leftChar}},
2282 {"rightChar", "Move cursor right one character.", 0, {rightChar}},
2283 {"splitH", "Split box in half horizontally.", 0, {halveBoxHorizontally}},
2284 {"splitLine", "Split line at cursor.", 0, {splitLine}},
2285 {"splitV", "Split box in half vertically.", 0, {halveBoxVertically}},
2286 {"startOfLine", "Go to start of line.", 0, {startOfLine}},
2287 {"startOfNLine", "Go to start of next line.", 0, {viStartOfNextLine}},
2288 {"switchBoxes", "Switch to another box.", 0, {switchBoxes}},
2289 {"upLine", "Move cursor up one line.", 0, {upLine}},
2290 {"upPage", "Move cursor up one page.", 0, {upPage}},
2291 {NULL, NULL, 0, {NULL}}
2292};
2293
2294struct keyCommand simpleViNormalKeys[] =
2295{
2296 {"BS", "leftChar"},
2297 {"X", "backSpaceChar"},
2298 {"Del", "deleteChar"},
2299 {"x", "deleteChar"},
2300 {"Down", "downLine"},
2301 {"j", "downLine"},
2302 {"End", "endOfLine"},
2303 {"Home", "startOfLine"},
2304 {"Left", "leftChar"},
2305 {"h", "leftChar"},
2306 {"PgDn", "downPage"},
2307 {"^F", "downPage"},
2308 {"PgUp", "upPage"},
2309 {"^B", "upPage"},
2310 {"Enter", "startOfNextLine"},
2311 {"Return", "startOfNextLine"},
2312 {"Right", "rightChar"},
2313 {"l", "rightChar"},
2314 {"i", "insert"},
2315 {":", "exMode"}, // This is the temporary ex mode that you can backspace out of. Or any command backs you out.
2316 {"Q", "exMode"}, // This is the ex mode you need to do the "visual" command to get out of.
2317 {"^Wv", "splitV"},
2318 {"^W^V", "splitV"},
2319 {"^Ws", "splitH"},
2320 {"^WS", "splitH"},
2321 {"^W^S", "splitH"},
2322 {"^Ww", "switchBoxes"},
2323 {"^W^W", "switchBoxes"},
2324 {"^Wq", "deleteBox"},
2325 {"^W^Q", "deleteBox"},
2326 {"Up", "upLine"},
2327 {"k", "upLine"},
2328 {NULL, NULL}
2329};
2330
2331struct keyCommand simpleViInsertKeys[] =
2332{
2333 {"BS", "backSpaceChar"},
2334 {"Del", "deleteChar"},
2335 {"Return", "splitLine"},
2336 {"Esc", "visual"},
2337 {"^C", "visual"},
2338 {NULL, NULL}
2339};
2340
2341struct keyCommand simpleExKeys[] =
2342{
2343 {"BS", "backSpaceChar"},
2344 {"Del", "deleteChar"},
2345 {"Down", "downLine"},
2346 {"End", "endOfLine"},
2347 {"Home", "startOfLine"},
2348 {"Left", "leftChar"},
2349 {"Enter", "executeLine"},
2350 {"Return", "executeLine"},
2351 {"Right", "rightChar"},
2352 {"Esc", "visual"},
2353 {"Up", "upLine"},
2354 {NULL, NULL}
2355};
2356
2357struct mode simpleViMode[] =
2358{
2359 {simpleViNormalKeys, NULL, NULL, 0},
2360 {simpleViInsertKeys, NULL, NULL, 0},
2361 {simpleExKeys, NULL, NULL, 1},
2362 {NULL, NULL, NULL}
2363};
2364
2365struct context simpleVi =
2366{
2367 simpleViCommands,
2368 simpleViMode,
2369 NULL,
2370 NULL,
2371 NULL
2372};
2373
2374
2375// TODO - simple sed editor? May be out of scope for "simple", so leave it until later?
2376// Probably entirely useless for "simple".
2377
2378
2379// TODO - have any unrecognised escape key sequence start up a new box (split one) to show the "show keys" content.
2380// That just adds each "Key is X" to the end of the content, and allows scrolling, as well as switching between other boxes.
2381
2382void boxes_main(void)
2383{
2384 struct context *context = &simpleMcedit; // The default is mcedit, coz that's what I use.
2385 struct termios termio, oldtermio;
2386 char *prompt = "Enter a command : ";
2387 unsigned W = 80, H = 24;
2388
2389 // For testing purposes, figure out which context we use. When this gets real, the toybox multiplexer will sort this out for us instead.
2390 if (toys.optflags & FLAG_m)
2391 {
2392 if (strcmp(TT.mode, "emacs") == 0)
2393 context = &simpleEmacs;
2394 else if (strcmp(TT.mode, "joe") == 0)
2395 context = &simpleJoe;
2396 else if (strcmp(TT.mode, "less") == 0)
2397 context = &simpleLess;
2398 else if (strcmp(TT.mode, "mcedit") == 0)
2399 context = &simpleMcedit;
2400 else if (strcmp(TT.mode, "more") == 0)
2401 context = &simpleMore;
2402 else if (strcmp(TT.mode, "nano") == 0)
2403 context = &simpleNano;
2404 else if (strcmp(TT.mode, "vi") == 0)
2405 context = &simpleVi;
2406 }
2407
2408 // TODO - Should do an isatty() here, though not sure about the usefullness of driving this from a script or redirected input, since it's supposed to be a UI for terminals.
2409 // It would STILL need the terminal size for output though. Perhaps just bitch and abort if it's not a tty?
2410 // On the other hand, sed don't need no stinkin' UI. And things like more or less should be usable on the end of a pipe.
2411
2412 // Grab the old terminal settings and save it.
2413 tcgetattr(0, &oldtermio);
2414 tcflush(0, TCIFLUSH);
2415 termio = oldtermio;
2416
2417 // Mould the terminal to our will.
2418 /*
2419 IUCLC (not in POSIX) Map uppercase characters to lowercase on input.
2420 IXON Enable XON/XOFF flow control on output.
2421 IXOFF Enable XON/XOFF flow control on input.
2422 IXANY (not in POSIX.1; XSI) Enable any character to restart output.
2423
2424 ECHO Echo input characters.
2425 ECHOE If ICANON is also set, the ERASE character erases the preceding input character, and WERASE erases the preceding word.
2426 ECHOK If ICANON is also set, the KILL character erases the current line.
2427 ECHONL If ICANON is also set, echo the NL character even if ECHO is not set.
2428 TOSTOP Send the SIGTTOU signal to the process group of a background process which tries to write to its controlling terminal.
2429 ICANON Enable canonical mode. This enables the special characters EOF, EOL, EOL2, ERASE, KILL, LNEXT, REPRINT, STATUS, and WERASE, and buffers by lines.
2430
2431 VTIME Timeout in deciseconds for non-canonical read.
2432 VMIN Minimum number of characters for non-canonical read.
2433
2434 raw mode turning off ICANON, IEXTEN, and ISIG kills most special key processing.
2435 termio.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON);
2436 termio.c_oflag &= ~OPOST;
2437 termio.c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN);
2438 termio.c_cflag &= ~(CSIZE | PARENB);
2439 termio.c_cflag |= CS8;
2440
2441 IGNBRK ignore BREAK
2442 BRKINT complicated, bet in this context, sends BREAK as '\x00'
2443 PARMRK characters with parity or frame erors are sent as '\x00'
2444 ISTRIP strip 8th byte
2445 INLCR translate LF to CR in input
2446 IGLCR ignore CR
2447 OPOST enable implementation defined output processing
2448 ISIG generate signals on INTR (SIGINT on ^C), QUIT (SIGQUIT on ^\\), SUSP (SIGTSTP on ^Z), DSUSP (SIGTSTP on ^Y)
2449 IEXTEN enable implementation defined input processing, turns on some key -> signal -tuff
2450 CSIZE mask for character sizes, so in this case, we mask them all out
2451 PARENB enable parity
2452 CS8 8 bit characters
2453
2454 VEOF "sends" EOF on ^D, ICANON turns that on.
2455 VSTART restart output on ^Q, IXON turns that on.
2456 VSTATUS display status info and sends SIGINFO (STATUS) on ^T. Not in POSIX, not supported in Linux. ICANON turns that on.
2457 VSTOP stop output on ^S, IXON turns that on.
2458 */
2459 termio.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IUCLC | IXON | IXOFF | IXANY);
2460 termio.c_oflag &= ~OPOST;
2461 termio.c_lflag &= ~(ECHO | ECHOE | ECHOK | ECHONL | TOSTOP | ICANON | ISIG | IEXTEN);
2462 termio.c_cflag &= ~(CSIZE | PARENB);
2463 termio.c_cflag |= CS8;
2464 termio.c_cc[VTIME]=0; // deciseconds.
2465 termio.c_cc[VMIN]=1;
2466 tcsetattr(0, TCSANOW, &termio);
2467
2468 terminal_size(&W, &H);
2469 if (toys.optflags & FLAG_w)
2470 W = TT.w;
2471 if (toys.optflags & FLAG_h)
2472 H = TT.h;
2473
2474 // Create the main box. Right now the system needs one for wrapping around while switching. The H - 1 bit is to leave room for our example command line.
2475 rootBox = addBox("root", context, toys.optargs[0], 0, 0, W, H - 1);
2476 currentBox = rootBox;
2477
2478 // Create the command line view, sharing the same context as the root. It will differentiate based on the view mode of the current box.
2479 // Also load the command line history as it's file.
2480 // TODO - different contexts will have different history files, though what to do about ones with no history, and ones with different histories for different modes?
2481 commandLine = addView("command", rootBox->view->content->context, ".boxes.history", 0, H, W, 1);
2482 // Add a prompt to it.
2483 commandLine->prompt = xrealloc(commandLine->prompt, strlen(prompt) + 1);
2484 strcpy(commandLine->prompt, prompt);
2485 // Move to the end of the history.
2486 moveCursorAbsolute(commandLine, 0, commandLine->content->lines.length, 0, 0);
2487
2488 // All the mouse tracking methods suck one way or another. sigh
2489 // http://rtfm.etla.org/xterm/ctlseq.html documents xterm stuff, near the bottom is the mouse stuff.
2490 // http://leonerds-code.blogspot.co.uk/2012/04/wide-mouse-support-in-libvterm.html is helpful.
2491 // Enable mouse (VT200 normal tracking mode, UTF8 encoding). The limit is 2015. Seems to only be in later xterms.
2492// fputs("\x1B[?1005h", stdout);
2493 // Enable mouse (VT340 locator reporting mode). In theory has no limit. Wont actually work though.
2494 // On the other hand, only allows for four buttons, so only half a mouse wheel.
2495 // Responds with "\1B[e;p;r;c;p&w" where e is event type, p is a bitmap of buttons pressed, r and c are the mouse coords in decimal, and p is the "page number".
2496// fputs("\x1B[1;2'z\x1B[1;3'{", stdout);
2497 // Enable mouse (VT200 normal tracking mode). Has a limit of 256 - 32 rows and columns. An xterm exclusive I think, but works in roxterm at least. No wheel reports.
2498 // Responds with "\x1B[Mbxy" where x and y are the mouse coords, and b is bit encoded buttons and modifiers - 0=MB1 pressed, 1=MB2 pressed, 2=MB3 pressed, 3=release, 4=Shift, 8=Meta, 16=Control
2499// fputs("\x1B[?1000h", stdout);
2500// fflush(stdout);
2501
2502 calcBoxes(currentBox);
2503 drawBoxes(currentBox);
2504 // Do the first cursor update.
2505 updateLine(currentBox->view);
2506
2507 // Run the main loop.
2508 handle_keys((long) currentBox->view, handleEvent);
2509
2510 // TODO - Should remember to turn off mouse reporting when we leave.
2511
2512 // Restore the old terminal settings.
2513 tcsetattr(0, TCSANOW, &oldtermio);
2514
2515 puts("");
2516 fflush(stdout);
2517}
diff --git a/src/boxes/dumbsh.c b/src/boxes/dumbsh.c
new file mode 100644
index 0000000..9ad0204
--- /dev/null
+++ b/src/boxes/dumbsh.c
@@ -0,0 +1,283 @@
1/* dumbsh.c - A really dumb shell, to demonstrate handle_keys usage.
2 *
3 * Copyright 2014 David Seikel <won_fang@yahoo.com.au>
4 *
5 * Not a real shell, so doesn't follow any standards,
6 * coz it wont implement them anyway.
7
8USE_DUMBSH(NEWTOY(dumbsh, "", TOYFLAG_USR|TOYFLAG_BIN))
9
10config DUMBSH
11 bool "dumbsh"
12 default n
13 help
14 usage: dumbsh
15
16 A really dumb shell.
17*/
18
19#include "toys.h"
20#include "lib/handlekeys.h"
21
22typedef void (*eventHandler) (void);
23
24struct keyCommand
25{
26 char *key;
27 eventHandler handler;
28};
29
30GLOBALS(
31 unsigned h, w;
32 int x, y;
33 struct double_list *history;
34)
35
36#define TT this.dumbsh
37
38// Sanity check cursor location and update the current line.
39static void updateLine()
40{
41 if (0 > TT.x) TT.x = 0;
42 if (0 > TT.y) TT.y = 0;
43 if (TT.w < TT.x) TT.x = TT.w;
44 if (TT.h < TT.y) TT.y = TT.h;
45 if (strlen(toybuf) < TT.x) TT.x = strlen(toybuf);
46 printf("\x1B[%d;0H%-*s\x1B[%d;%dH",
47 TT.y + 1, TT.w, toybuf, TT.y + 1, TT.x + 1);
48 fflush(stdout);
49}
50
51// The various commands.
52static void deleteChar()
53{
54 int j;
55
56 for (j = TT.x; toybuf[j]; j++)
57 toybuf[j] = toybuf[j + 1];
58 updateLine();
59}
60
61static void backSpaceChar()
62{
63 if (TT.x)
64 {
65 TT.x--;
66 deleteChar();
67 }
68}
69
70// This is where we would actually deal with
71// what ever command the user had typed in.
72// For now we just move on to the next line.
73// TODO - We would want to redirect I/O, capture some keys (^C),
74// but pass the rest on.
75// Dunno yet how to deal with that.
76// We still want handle_keys to be doing it's thing,
77// so maybe handing it another fd, and a callback.
78// A function to add and remove fd and callback pairs for
79// handle_keys to check?
80static void doCommand()
81{
82 toybuf[0] = 0;
83 TT.x = 0;
84 TT.y++;
85 printf("\n");
86 fflush(stdout);
87 updateLine();
88}
89
90static void endOfLine()
91{
92 TT.x = strlen(toybuf);
93 updateLine();
94}
95
96static void leftChar()
97{
98 TT.x--;
99 updateLine();
100}
101
102static void nextHistory()
103{
104 TT.history = TT.history->next;
105 strcpy(toybuf, TT.history->data);
106 TT.x = strlen(toybuf);
107 updateLine();
108}
109
110static void prevHistory()
111{
112 TT.history = TT.history->prev;
113 strcpy(toybuf, TT.history->data);
114 TT.x = strlen(toybuf);
115 updateLine();
116}
117
118static void quit()
119{
120 handle_keys_quit();
121}
122
123static void rightChar()
124{
125 TT.x++;
126 updateLine();
127}
128
129static void startOfLine()
130{
131 TT.x = 0;
132 updateLine();
133}
134
135// The key to command mappings, Emacs style.
136static const struct keyCommand simpleEmacsKeys[] =
137{
138 {"BS", backSpaceChar},
139 {"Del", deleteChar},
140 {"^D", deleteChar},
141 {"Return", doCommand},
142 {"Enter", doCommand},
143 {"Down", nextHistory},
144 {"^N", nextHistory},
145 {"End", endOfLine},
146 {"^E", endOfLine},
147 {"Left", leftChar},
148 {"^B", leftChar},
149 {"^X^C", quit},
150 {"^C", quit},
151 {"Right", rightChar},
152 {"^F", rightChar},
153 {"Home", startOfLine},
154 {"^A", startOfLine},
155 {"Up", prevHistory},
156 {"^P", prevHistory}
157};
158
159// Callback for incoming sequences from the terminal.
160static int handleEvent(long extra, struct keyevent *event)
161{
162 switch (event->type)
163 {
164 case HK_KEYS :
165 {
166 int j, l = strlen(event->sequence);
167
168 // Search for a key sequence bound to a command.
169 for (j = 0; j < ARRAY_LEN(simpleEmacsKeys); j++)
170 {
171 if (strncmp(simpleEmacsKeys[j].key, event->sequence, l) == 0)
172 {
173 // If it's a partial match, keep accumulating them.
174 if (strlen(simpleEmacsKeys[j].key) != l)
175 return 0;
176 else
177 {
178 if (simpleEmacsKeys[j].handler) simpleEmacsKeys[j].handler();
179 return 1;
180 }
181 }
182 }
183
184 // See if it's ordinary keys.
185 // NOTE - with vi style ordinary keys can be commands,
186 // but they would be found by the command check above first.
187 if (!event->isTranslated)
188 {
189 if (TT.x < sizeof(toybuf))
190 {
191 int j, l = strlen(event->sequence);
192
193 for (j = strlen(toybuf); j >= TT.x; j--)
194 toybuf[j + l] = toybuf[j];
195 for (j = 0; j < l; j++)
196 toybuf[TT.x + j] = event->sequence[j];
197 TT.x += l;
198 updateLine();
199 }
200 }
201 break;
202 }
203
204 case HK_CSI :
205 {
206 // Is it a cursor location report?
207 if (strcmp("R", event->sequence) == 0)
208 {
209 // Parameters are cursor line and column.
210 // NOTE - This may be sent at other times, not just during terminal resize.
211 // We are assuming here that it's a resize.
212 // The defaults are 1, which get ignored by the heuristic below.
213 int r = event->params[0], c = event->params[1];
214
215 // Check it's not an F3 key variation, coz some of them use
216 // the same CSI function command.
217 // This is a heuristic, we are checking against an unusable terminal size.
218 if ((2 == event->count) && (8 < r) && (8 < c))
219 {
220 TT.h = r;
221 TT.w = c;
222 updateLine();
223 }
224 break;
225 }
226 }
227
228 default : break;
229 }
230
231 // Tell handle_keys to drop it, coz we dealt with it, or it's not one of ours.
232 return 1;
233}
234
235void dumbsh_main(void)
236{
237 struct termios termIo, oldTermIo;
238 char *t = getenv("HOME");
239 int fd;
240
241 // Load bash history.
242 t = xmprintf("%s/%s", t ? t : "", ".bash_history");
243 if (-1 != (fd = open(t, O_RDONLY)))
244 {
245 while ((t = get_line(fd))) TT.history = dlist_add(&TT.history, t);
246 close(fd);
247 }
248 if (!TT.history)
249 TT.history = dlist_add(&TT.history, "");
250
251 // Grab the old terminal settings and save it.
252 tcgetattr(0, &oldTermIo);
253 tcflush(0, TCIFLUSH);
254 termIo = oldTermIo;
255
256 // Mould the terminal to our will.
257 // In this example we are turning off all the terminal smarts, but real code
258 // might not want that.
259 termIo.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL
260 | IUCLC | IXON | IXOFF | IXANY);
261 termIo.c_oflag &= ~OPOST;
262 termIo.c_lflag &= ~(ECHO | ECHOE | ECHOK | ECHONL | TOSTOP | ICANON | ISIG
263 | IEXTEN);
264 termIo.c_cflag &= ~(CSIZE | PARENB);
265 termIo.c_cflag |= CS8;
266 termIo.c_cc[VTIME]=0; // deciseconds.
267 termIo.c_cc[VMIN]=1;
268 tcsetattr(0, TCSANOW, &termIo);
269
270 // Let the mouldy old terminal mold us.
271 TT.w = 80;
272 TT.h = 24;
273 terminal_size(&TT.w, &TT.h);
274
275 // Let's rock!
276 updateLine();
277 handle_keys(0, handleEvent);
278
279 // Clean up.
280 tcsetattr(0, TCSANOW, &oldTermIo);
281 puts("");
282 fflush(stdout);
283}
diff --git a/src/boxes/handlekeys.c b/src/boxes/handlekeys.c
new file mode 100644
index 0000000..8bae529
--- /dev/null
+++ b/src/boxes/handlekeys.c
@@ -0,0 +1,445 @@
1/* handlekeys.c - Generic terminal input handler.
2 *
3 * Copyright 2012 David Seikel <won_fang@yahoo.com.au>
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 "handlekeys.h"
14
15struct key
16{
17 char *code;
18 char *name;
19};
20
21// This table includes some variations I have found on some terminals.
22// http://rtfm.etla.org/xterm/ctlseq.html has a useful guide.
23// TODO - Don't think I got all the linux console or xterm variations.
24// TODO - Add more shift variations, plus Ctrl & Alt variations when needed.
25// TODO - tmux messes with the shift function keys somehow.
26// TODO - Add other miscelany that does not use an escape sequence.
27
28// This is sorted by type, though there is some overlap.
29// Human typing speeds wont need fast searching speeds on this small table.
30// So simple wins out over speed, and sorting by terminal type wins
31// the simple test.
32static struct key keys[] =
33{
34 // Control characters.
35 // Commented out coz it's the C string terminator, and may confuse things.
36 //{"\x00", "^@"}, // NUL
37 {"\x01", "^A"}, // SOH Apparently sometimes sent as Home.
38 {"\x02", "^B"}, // STX
39 {"\x03", "^C"}, // ETX SIGINT Emacs and vi.
40 {"\x04", "^D"}, // EOT EOF Emacs, joe, and nano.
41 {"\x05", "^E"}, // ENQ Apparently sometimes sent as End
42 {"\x06", "^F"}, // ACK
43 {"\x07", "^G"}, // BEL
44 {"\x08", "Del"}, // BS Delete key, usually.
45 {"\x09", "Tab"}, // HT
46 {"\x0A", "Enter"}, // LF Roxterm translates Ctrl-M to this.
47 {"\x0B", "^K"}, // VT
48 {"\x0C", "^L"}, // FF
49 {"\x0D", "Return"}, // CR Other Enter/Return key, usually.
50 {"\x0E", "^N"}, // SO
51 {"\x0F", "^O"}, // SI DISCARD
52 {"\x10", "^P"}, // DLE
53 {"\x11", "^Q"}, // DC1 SIGCONT Vi.
54 {"\x12", "^R"}, // DC2
55 {"\x13", "^S"}, // DC3 SIGSTOP can't be caught. Emacs and vi.
56 {"\x14", "^T"}, // DC4 SIGINFO STATUS
57 {"\x15", "^U"}, // NAK KILL character
58 {"\x16", "^V"}, // SYN LNEXT
59 {"\x17", "^W"}, // ETB WERASE
60 {"\x18", "^X"}, // CAN KILL character
61 {"\x19", "^Y"}, // EM DSUSP SIGTSTP
62 {"\x1A", "^Z"}, // SUB SIGTSTP
63 // Commented out coz it's the ANSI start byte in the below multibyte keys.
64 // Handled in the code with a timeout.
65 //{"\x1B", "Esc"}, // ESC Esc key.
66 {"\x1C", "^\\"}, // FS SIGQUIT
67 {"\x1D", "^]"}, // GS
68 {"\x1E", "^^"}, // RS
69 {"\x1F", "^_"}, // US
70 {"\x7F", "BS"}, // Backspace key, usually. Ctrl-? perhaps?
71 // Commented out for the same reason Esc is.
72 //{"\x9B", "CSI"}, // CSI The eight bit encoding of "Esc [".
73
74 // "Usual" xterm CSI sequences, with ";1" omitted for no modifiers.
75 // Even though we have a proper CSI parser,
76 // these should still be in this table. Coz we would need a table anyway
77 // in the CSI parser, so might as well keep them with the others.
78 // Also, less code, no need to have a separate scanner for that other table.
79 {"\x9B\x31~", "Home"}, // Duplicate, think I've seen this somewhere.
80 {"\x9B\x32~", "Ins"},
81 {"\x9B\x33~", "Del"},
82 {"\x9B\x34~", "End"}, // Duplicate, think I've seen this somewhere.
83 {"\x9B\x35~", "PgUp"},
84 {"\x9B\x36~", "PgDn"},
85 {"\x9B\x37~", "Home"},
86 {"\x9B\x38~", "End"},
87 {"\x9B\x31\x31~", "F1"},
88 {"\x9B\x31\x32~", "F2"},
89 {"\x9B\x31\x33~", "F3"},
90 {"\x9B\x31\x34~", "F4"},
91 {"\x9B\x31\x35~", "F5"},
92 {"\x9B\x31\x37~", "F6"},
93 {"\x9B\x31\x38~", "F7"},
94 {"\x9B\x31\x39~", "F8"},
95 {"\x9B\x32\x30~", "F9"},
96 {"\x9B\x32\x31~", "F10"},
97 {"\x9B\x32\x33~", "F11"},
98 {"\x9B\x32\x34~", "F12"},
99
100 // As above, ";2" means shift modifier.
101 {"\x9B\x31;2~", "Shift Home"},
102 {"\x9B\x32;2~", "Shift Ins"},
103 {"\x9B\x33;2~", "Shift Del"},
104 {"\x9B\x34;2~", "Shift End"},
105 {"\x9B\x35;2~", "Shift PgUp"},
106 {"\x9B\x36;2~", "Shift PgDn"},
107 {"\x9B\x37;2~", "Shift Home"},
108 {"\x9B\x38;2~", "Shift End"},
109 {"\x9B\x31\x31;2~", "Shift F1"},
110 {"\x9B\x31\x32;2~", "Shift F2"},
111 {"\x9B\x31\x33;2~", "Shift F3"},
112 {"\x9B\x31\x34;2~", "Shift F4"},
113 {"\x9B\x31\x35;2~", "Shift F5"},
114 {"\x9B\x31\x37;2~", "Shift F6"},
115 {"\x9B\x31\x38;2~", "Shift F7"},
116 {"\x9B\x31\x39;2~", "Shift F8"},
117 {"\x9B\x32\x30;2~", "Shift F9"},
118 {"\x9B\x32\x31;2~", "Shift F10"},
119 {"\x9B\x32\x33;2~", "Shift F11"},
120 {"\x9B\x32\x34;2~", "Shift F12"},
121
122 // "Normal" Some terminals are special, and it seems they only have
123 // four function keys.
124 {"\x9B\x41", "Up"},
125 {"\x9B\x42", "Down"},
126 {"\x9B\x43", "Right"},
127 {"\x9B\x44", "Left"},
128 {"\x9B\x46", "End"},
129 {"\x9BH", "Home"},
130 {"\x9BP", "F1"},
131 {"\x9BQ", "F2"},
132 {"\x9BR", "F3"},
133 {"\x9BS", "F4"},
134 {"\x9B\x31;2P", "Shift F1"},
135 {"\x9B\x31;2Q", "Shift F2"},
136 {"\x9B\x31;2R", "Shift F3"},
137 {"\x9B\x31;2S", "Shift F4"},
138
139 // "Application" Esc O is known as SS3
140 {"\x1BOA", "Up"},
141 {"\x1BOB", "Down"},
142 {"\x1BOC", "Right"},
143 {"\x1BOD", "Left"},
144 {"\x1BOF", "End"},
145 {"\x1BOH", "Home"},
146 {"\x1BOn", "Del"},
147 {"\x1BOp", "Ins"},
148 {"\x1BOq", "End"},
149 {"\x1BOw", "Home"},
150 {"\x1BOP", "F1"},
151 {"\x1BOQ", "F2"},
152 {"\x1BOR", "F3"},
153 {"\x1BOS", "F4"},
154 {"\x1BOT", "F5"},
155 // These two conflict with the above four function key variations.
156 {"\x9BR", "F6"},
157 {"\x9BS", "F7"},
158 {"\x9BT", "F8"},
159 {"\x9BU", "F9"},
160 {"\x9BV", "F10"},
161 {"\x9BW", "F11"},
162 {"\x9BX", "F12"},
163
164 // Can't remember, but saw them somewhere.
165 {"\x1BO1;2P", "Shift F1"},
166 {"\x1BO1;2Q", "Shift F2"},
167 {"\x1BO1;2R", "Shift F3"},
168 {"\x1BO1;2S", "Shift F4"},
169};
170
171static volatile sig_atomic_t sigWinch;
172static int stillRunning;
173
174static void handleSIGWINCH(int signalNumber)
175{
176 sigWinch = 1;
177}
178
179void handle_keys(long extra, int (*handle_event)(long extra, struct keyevent *event))
180{
181 struct keyevent event;
182 fd_set selectFds;
183 struct timespec timeOut;
184 struct sigaction sigAction, oldSigAction;
185 sigset_t signalMask;
186 char buffer[20], sequence[20];
187 int buffIndex = 0, pendingEsc = 0;
188
189 buffer[0] = 0;
190 sequence[0] = 0;
191
192 // Terminals send the SIGWINCH signal when they resize.
193 memset(&sigAction, 0, sizeof(sigAction));
194 sigAction.sa_handler = handleSIGWINCH;
195 sigAction.sa_flags = SA_RESTART; // Useless if we are using poll.
196 if (sigaction(SIGWINCH, &sigAction, &oldSigAction))
197 perror_exit("can't set signal handler for SIGWINCH");
198 sigemptyset(&signalMask);
199 sigaddset(&signalMask, SIGWINCH);
200
201 // TODO - OS buffered keys might be a problem, but we can't do the
202 // usual timestamp filter for now.
203
204 stillRunning = 1;
205 while (stillRunning)
206 {
207 int j, p, csi = 0;
208
209 // Apparently it's more portable to reset these each time.
210 FD_ZERO(&selectFds);
211 FD_SET(0, &selectFds);
212 timeOut.tv_sec = 0; timeOut.tv_nsec = 100000000; // One tenth of a second.
213
214 // We got a "terminal size changed" signal, ask the terminal
215 // how big it is now.
216 if (sigWinch)
217 {
218 // Send - save cursor position, down 999, right 999,
219 // request cursor position, restore cursor position.
220 fputs("\x1B[s\x1B[999C\x1B[999B\x1B[6n\x1B[u", stdout);
221 fflush(stdout);
222 sigWinch = 0;
223 }
224
225 // TODO - Should only ask for a time out after we get an Escape, or
226 // the user requested time ticks.
227 // I wanted to use poll, but that would mean using ppoll, which is
228 // Linux only, and involves defining swear words to get it.
229 p = pselect(0 + 1, &selectFds, NULL, NULL, &timeOut, &signalMask);
230 if (0 > p)
231 {
232 if (EINTR == errno)
233 continue;
234 perror_exit("poll");
235 }
236 else if (0 == p) // A timeout, trigger a time event.
237 {
238 if (pendingEsc)
239 {
240 // After a short delay to check, this is a real Escape key,
241 // not part of an escape sequence, so deal with it.
242 strcat(sequence, "Esc");
243 buffer[0] = buffIndex = 0;
244 }
245 // TODO - Call some sort of timer tick callback. This wont be
246 // a precise timed event, but don't think we need one.
247 }
248 else if ((0 < p) && FD_ISSET(0, &selectFds))
249 {
250 j = xread(0, &buffer[buffIndex], sizeof(buffer) - (buffIndex + 1));
251 if (j == 0) // End of file.
252 stillRunning = 0;
253 else
254 {
255 buffIndex += j;
256 buffer[buffIndex] = 0;
257
258 // Send raw keystrokes, mostly for things like showkey.
259 event.type = HK_RAW;
260 event.sequence = buffer;
261 event.isTranslated = 0;
262 handle_event(extra, &event);
263
264 if (sizeof(buffer) < (buffIndex + 1)) // Ran out of buffer.
265 {
266 buffer[buffIndex] = 0;
267 fprintf(stderr, "Full buffer - %s -> %s\n", buffer, sequence);
268 for (j = 0; buffer[j]; j++)
269 fprintf(stderr, "(%x) %c, ", (int) buffer[j], buffer[j]);
270 fflush(stderr);
271 buffer[0] = buffIndex = 0;
272 }
273 }
274 }
275
276 // Check for lone Esc first, wait a bit longer if it is.
277 pendingEsc = ((0 == buffer[1]) && ('\x1B' == buffer[0]));
278 if (pendingEsc) continue;
279
280 // Check if it's a CSI before we check for the known key sequences.
281 // C29B is the UTF8 encoding of CSI.
282 // In all cases we reduce CSI to 9B to keep the keys table shorter.
283 if ((('\x1B' == buffer[0]) && ('[' == buffer[1]))
284 || (('\xC2' == buffer[0]) && ('\x9B' == buffer[1])))
285 {
286 buffer[0] = '\x9B';
287 for (j = 1; buffer[j]; j++)
288 buffer[j] = buffer[j + 1];
289 buffIndex--;
290 }
291 csi = ('\x9B' == buffer[0]);
292
293 // Check for known key sequences.
294 // For a real timeout checked Esc, buffer is now empty, so this for loop
295 // wont find it anyway. While it's true we could avoid it by checking,
296 // the user already had to wait for a time out, and this loop wont take THAT long.
297 for (j = 0; j < ARRAY_LEN(keys); j++)
298 {
299 if (strcmp(keys[j].code, buffer) == 0)
300 {
301 strcat(sequence, keys[j].name);
302 buffer[0] = buffIndex = 0;
303 csi = 0;
304 break;
305 }
306 }
307
308 // Find out if it's a CSI sequence that's not in the known key sequences.
309 if (csi)
310 {
311 /* ECMA-048 section 5.2 defines this, and is unreadable.
312 * So I'll include some notes here that tries to simplify that.
313 *
314 * The CSI format is - CSI [private] n1 ; n2 [extra] final
315 * Each of those parts, except for the initial CSI bytes, is an ordinary
316 * ASCII character.
317 *
318 * The optional [private] part is one of these characters "<=>?".
319 * If the first byte is one of these, then this is a private command, if
320 * it's one of the other n1 ones, it's not private.
321 *
322 * Next is a semi colon separated list of parameters (n1, n2, etc), which
323 * can be any characters from this set "01234567890:;<=>?". What the non
324 * digit ones mean is up to the command. Parameters can be left out, but
325 * their defaults are command dependant.
326 *
327 * Next is an optional [extra] part from this set of characters
328 * "!#$%&'()*+,-./", which includes double quotes. Can be many of these,
329 * likely isn't.
330 *
331 * Finally is the "final" from this set of characters "@[\]^_`{|}~", plus
332 * upper and lower case letters. It's private if it's one of these
333 * "pqrstuvwxyz{|}~". Though the "private" ~ is used for key codes.
334 *
335 * A full CSI command is the private, extra, and final parts.
336 *
337 * Any C0 controls, DEL (0x7f), or higher characters are undefined.
338 * TODO - So abort the current CSI and start from scratch on one of those.
339 */
340
341 if ('M' == buffer[1])
342 {
343 // We have a mouse report, which is CSI M ..., where the rest is
344 // binary encoded, more or less. Not fitting into the CSI format.
345 // To make things worse, can't tell how long this will be.
346 // So leave it up to the caller to tell us if they used it.
347 event.type = HK_MOUSE;
348 event.sequence = buffer;
349 event.isTranslated = 0;
350 if (handle_event(extra, &event))
351 {
352 buffer[0] = buffIndex = 0;
353 sequence[0] = 0;
354 }
355 }
356 else
357 {
358 char *t, csFinal[8];
359 int csIndex = 1, csParams[8];
360
361 csFinal[0] = 0;
362 p = 0;
363
364 // Unspecified params default to a value that is command dependant.
365 // However, they will never be negative, so we can use -1 to flag
366 // a default value.
367 for (j = 0; j < ARRAY_LEN(csParams); j++)
368 csParams[j] = -1;
369
370 // Check for the private bit.
371 if (index("<=>?", buffer[1]))
372 {
373 csFinal[0] = buffer[1];
374 csFinal[1] = 0;
375 csIndex++;
376 }
377
378 // Decode parameters.
379 j = csIndex;
380 do
381 {
382 // So we know when we get to the end of parameter space.
383 t = index("01234567890:;<=>?", buffer[j + 1]);
384 // See if we passed a paremeter.
385 if ((';' == buffer[j]) || (!t))
386 {
387 // Only stomp on the ; if it's really the ;.
388 if (t)
389 buffer[j] = 0;
390 // Empty parameters are default parameters, so only deal with
391 // non defaults.
392 if (';' != buffer[csIndex] || (!t))
393 {
394 // TODO - Might be ":" in the number somewhere, but we are not
395 // expecting any in anything we do.
396 csParams[p] = atoi(&buffer[csIndex]);
397 }
398 p++;
399 csIndex = j + 1;
400 }
401 j++;
402 }
403 while (t);
404
405 // Check if we got the final byte, and send it to the callback.
406 strcat(csFinal, &buffer[csIndex]);
407 t = csFinal + strlen(csFinal) - 1;
408 if (('\x40' <= (*t)) && ((*t) <= '\x7e'))
409 {
410 event.type = HK_CSI;
411 event.sequence = csFinal;
412 event.isTranslated = 1;
413 event.count = p;
414 event.params = csParams;
415 handle_event(extra, &event);
416 buffer[0] = buffIndex = 0;
417 sequence[0] = 0;
418 }
419 }
420 }
421
422 // Pass the result to the callback.
423 if (sequence[0] || buffer[0])
424 {
425 char b[strlen(sequence) + strlen(buffer) + 1];
426
427 sprintf(b, "%s%s", sequence, buffer);
428 event.type = HK_KEYS;
429 event.sequence = b;
430 event.isTranslated = (0 != sequence[0]);
431 if (handle_event(extra, &event))
432 {
433 buffer[0] = buffIndex = 0;
434 sequence[0] = 0;
435 }
436 }
437 }
438
439 sigaction(SIGWINCH, &oldSigAction, NULL);
440}
441
442void handle_keys_quit()
443{
444 stillRunning = 0;
445}
diff --git a/src/boxes/handlekeys.h b/src/boxes/handlekeys.h
new file mode 100644
index 0000000..868183f
--- /dev/null
+++ b/src/boxes/handlekeys.h
@@ -0,0 +1,76 @@
1/* handlekeys.h - Generic terminal input handler.
2 *
3 * Copyright 2012 David Seikel <won_fang@yahoo.com.au>
4 */
5
6enum keyeventtype{
7 HK_CSI,
8 HK_KEYS,
9 HK_MOUSE,
10 HK_RAW
11};
12
13struct keyevent {
14 enum keyeventtype 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/* An input loop that handles keystrokes and terminal CSI commands.
22 *
23 * Reads stdin, trying to translate raw keystrokes into something more readable.
24 * See the keys[] array at the top of handlekeys.c for what byte sequences get
25 * translated into what key names. See dumbsh.c for an example of usage.
26 * A 0.1 second delay is used to detect the Esc key being pressed, and not Esc
27 * being part of a raw keystroke.
28 *
29 * handle_keys also tries to decode CSI commands that terminals can send.
30 * Some keystrokes are CSI commands, but those are translated as key sequences
31 * instead of CSI commands.
32 *
33 * handle_keys also sets up a SIGWINCH handler to catch terminal resizes,
34 * and sends a request to the terminal to report it's current size when it gets
35 * a SIGWINCH. This is the main reason for HK_CSI, as those reports are
36 * sent as CSI. It's still up to the user code to recognise and deal with the
37 * terminal resize response, but at least it's nicely decoded for you.
38 *
39 * Arguments -
40 * extra - arbitrary data that gets passed back to the callbacks.
41 * handle_event - a callback to handle sequences.
42 *
43 * handle_event is called when a complete sequence has been accumulated. It is
44 * passed a keyevent structure. The type member of that structure determines
45 * what sort of event this is. What's in the rest of the keyevent depends on
46 * the type -
47 *
48 * HK_CSI
49 * sequence is the fully decoded CSI command, including the private and intermediate characters.
50 * isTranslated is 1, since the CSI command has been translated.
51 * count is the count of translated CSI parameters.
52 * params is an array of translateted CSI parameters.
53 * Empty parameters are set to -1, coz -1 parameters are not legal,
54 * and empty ones should default to something that is command dependant.
55 *
56 * HK_KEYS
57 * sequence the keystrokes as ASCII, either translated or not.
58 * isTranslated if 0, then sequence is ordinary keys, otherwise
59 * sequence is the names of keys, from the keys[] array.
60 * count and params are not used.
61 *
62 * For HK_KEYS handle_event should return 1 if the sequence has been dealt with,
63 * or ignored. It should return 0, if handle_keys should keep adding more
64 * translated keystroke sequences on the end, and try again later.
65 * 0 should really only be used if it's a partial match, and we need more
66 * keys in the sequence to make a full match.
67 *
68 * HK_MOUSE
69 * sequence is the raw bytes of the mouse report. The rest are not used.
70 *
71 */
72void handle_keys(long extra, int (*handle_event)(long extra, struct keyevent *event));
73
74
75/* Call this when you want handle_keys to return. */
76void handle_keys_quit();
diff --git a/src/boxes/showkey.c b/src/boxes/showkey.c
new file mode 100644
index 0000000..de1f804
--- /dev/null
+++ b/src/boxes/showkey.c
@@ -0,0 +1,149 @@
1/* showkey.c - Shows the keys pressed.
2 *
3 * Copyright 2014 David Seikel <won_fang@yahoo.com.au>
4 *
5 * Not actually a standard, seems to be three different versions.
6 * The original kbd - http://kbd-project.org/
7 * The kbd fork console-tools - http://lct.sourceforge.net/
8 * A utility invented by Eric S. Raymond - http://catb.org/esr/showkey/
9
10USE_SHOWKEY(NEWTOY(showkey, "", TOYFLAG_USR|TOYFLAG_BIN))
11
12config SHOWKEY
13 bool "showkey"
14 default n
15 help
16 usage: showkey
17
18 Shows the keys pressed.
19*/
20
21#include "toys.h"
22#include "lib/handlekeys.h"
23
24typedef void (*eventHandler) (void);
25
26struct keyCommand
27{
28 char *key;
29 eventHandler handler;
30};
31
32GLOBALS(
33 unsigned h, w;
34 int x, y;
35)
36
37#define TT this.showkey
38
39
40static void quit()
41{
42 printf("Quitting.\r\n");
43 handle_keys_quit();
44}
45
46// The key to command mappings.
47static struct keyCommand simpleKeys[] =
48{
49 {"^C", quit}
50};
51
52// Callback for incoming sequences from the terminal.
53static int handleEvent(long extra, struct keyevent *event)
54{
55 int i;
56
57 switch (event->type)
58 {
59 case HK_RAW :
60 {
61 printf("RAW ");
62 for (i = 0; event->sequence[i]; i++)
63 {
64 printf("(%x) ", (int) event->sequence[i]);
65 if (32 > event->sequence[i])
66 printf("^%c, ", (int) event->sequence[i] + 'A' - 1);
67 else
68 printf("%c, ", (int) event->sequence[i]);
69 }
70 printf("-> ");
71 break;
72 }
73
74 case HK_KEYS :
75 {
76 int l = strlen(event->sequence);
77
78 if (event->isTranslated)
79 printf("TRANSLATED - ");
80 else
81 printf("KEY - ");
82 printf("%s\r\n", event->sequence);
83
84 // Search for a key sequence bound to a command.
85 for (i = 0; i < ARRAY_LEN(simpleKeys); i++)
86 {
87 if (strncmp(simpleKeys[i].key, event->sequence, l) == 0)
88 {
89 // If it's a partial match, keep accumulating them.
90 if (strlen(simpleKeys[i].key) != l)
91 return 0;
92 else
93 if (simpleKeys[i].handler) simpleKeys[i].handler();
94 }
95 }
96 break;
97 }
98
99 case HK_CSI :
100 {
101 // Is it a cursor location report?
102 if (strcmp("R", event->sequence) == 0)
103 {
104 printf("CSI cursor position - line %d, column %d\r\n", event->params[0], event->params[1]);
105 return 1;
106 }
107
108 printf("CSI command %s - ", event->sequence);
109 for (i = 0; i < event->count; i++)
110 printf("%d ", event->params[i]);
111 printf("\r\n");
112 break;
113 }
114
115 default : break;
116 }
117
118 return 1;
119}
120
121void showkey_main(void)
122{
123 struct termios termIo, oldTermIo;
124
125 // Grab the old terminal settings and save it.
126 tcgetattr(0, &oldTermIo);
127 tcflush(0, TCIFLUSH);
128 termIo = oldTermIo;
129
130 // Mould the terminal to our will.
131 // In this example we are turning off all the terminal smarts, but real code
132 // might not want that.
133 termIo.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL
134 | IUCLC | IXON | IXOFF | IXANY);
135 termIo.c_oflag &= ~OPOST;
136 termIo.c_lflag &= ~(ECHO | ECHOE | ECHOK | ECHONL | TOSTOP | ICANON | ISIG
137 | IEXTEN);
138 termIo.c_cflag &= ~(CSIZE | PARENB);
139 termIo.c_cflag |= CS8;
140 termIo.c_cc[VTIME]=0; // deciseconds.
141 termIo.c_cc[VMIN]=1;
142 tcsetattr(0, TCSANOW, &termIo);
143
144 handle_keys(0, handleEvent);
145
146 tcsetattr(0, TCSANOW, &oldTermIo);
147 puts("");
148 fflush(stdout);
149}
diff --git a/src/build/fcgi2 b/src/build/fcgi2
new file mode 160000
Subproject 3a29b212f4885182ebb269e4bd3f12c76fc5ebb
diff --git a/src/build/luajit b/src/build/luajit
new file mode 160000
Subproject 0ad60ccbc3768fa8e3e726858adf261950edbc2
diff --git a/src/build/qlibc b/src/build/qlibc
new file mode 160000
Subproject 25d5f5ce44ec4c863edbeaecdcb4d3c05dcf3aa
diff --git a/src/build/toybox b/src/build/toybox
new file mode 160000
Subproject f01dcd6e57ec8a7f41fc7c413d4f524d7f21c15
diff --git a/src/git-sub-modules/README b/src/git-sub-modules/README
new file mode 100644
index 0000000..1cf4e1a
--- /dev/null
+++ b/src/git-sub-modules/README
@@ -0,0 +1,3 @@
1Put git sub modules here, and then add code to BuildIt.sh to move them to
2where they are needed, or link them. Poor man's git sub modules /
3subtrees, coz otherwise it gets complex.
diff --git a/src/git-sub-modules/fcgi2 b/src/git-sub-modules/fcgi2
new file mode 160000
Subproject ca92b995d836c84e0433c1550c82dbdc98e5237
diff --git a/src/git-sub-modules/luajit b/src/git-sub-modules/luajit
new file mode 160000
Subproject 0ad60ccbc3768fa8e3e726858adf261950edbc2
diff --git a/src/git-sub-modules/qlibc b/src/git-sub-modules/qlibc
new file mode 160000
Subproject 25d5f5ce44ec4c863edbeaecdcb4d3c05dcf3aa
diff --git a/src/git-sub-modules/toybox b/src/git-sub-modules/toybox
new file mode 160000
Subproject 5a6d1746baacb40e2a3f094af50dbe9871afa3c
diff --git a/src/miniconfig b/src/miniconfig
new file mode 100644
index 0000000..9f8fe88
--- /dev/null
+++ b/src/miniconfig
@@ -0,0 +1,13 @@
1CONFIG_BOXES=y
2CONFIG_HELP=y
3CONFIG_PS=y
4CONFIG_RM=y
5CONFIG_SH=y
6CONFIG_SU=y
7CONFIG_UUIDGEN=y
8CONFIG_SLEDJCHISL=y
9CONFIG_TOYBOX_FREE=y
10CONFIG_TOYBOX_HELP=y
11CONFIG_TOYBOX_HELP_DASHDASH=y
12CONFIG_TOYBOX_I18N=y
13CONFIG_TOYBOX_SUID=y
diff --git a/src/sledjchisl/README b/src/sledjchisl/README
new file mode 100644
index 0000000..31a4b30
--- /dev/null
+++ b/src/sledjchisl/README
@@ -0,0 +1,118 @@
1I'm re-purposing this for SledjHamr https://sledjhamr.org/git/docs/index.html
2
3The general structure of SledjHamr is a bunch of servers talking to each
4other via Internet (or just local) connections. One of them is a web
5server for assets, world data, and inventory. Actually most of OpenSim
6is just a collection of web servers.
7
8Originally I didn't think using a web based world client was a good idea,
9however it might be better to have one, for reasons. Now I need a web
10management console that can do all the things the current tmux console
11can, including OpenSim console and commands. Plus account management for
12users. I can also use a web based Jabber / XMPP front end to chat, IM,
13and group chatter, which would run in the normal viewers web browser.
14This provides a doorway into putting SledjHamr stuff in existing viewers
15without needing them to support it. So a web based viewer now makes more
16sense, and also means we can get away with not needing a viewer at all.
17
18Toybox itself doesn't include a web server, and I don't think there is
19one on the roadmap. So we have to use an external web server, which was
20a design goal of SledjHamr in the first place, using existing mature
21HTTP infrastructure, coz that's already solved problems for a bunch of
22things that plague OS/SL to this day. Clear your cache! Pffft.
23
24So sledjchisl.c will be the "love world server", though initially it just
25drives OpenSim_SC in tmux via tmux commands to send keys and read output.
26Later it might run opensim_SC directly and use STDIN and STDOUT to do
27everything. It'll also provide the text management front end that runs
28in the left tmux panel of the first window, which is why it's based on
29boxes in the first place. Later still it can take over opensim_SC
30functions as I move them out of mono.
31
32We will need a text, web, and GUI version of this management front end.
33Hmmm, maybe don't need a GUI version, GUI users can just run a terminal.
34
35After much research, FastCGI / FCGI seems to be the most portable way of
36interfacing with existing web servers. FCGI protocol closes STDERR and
37STDOUT, and uses STDIN as it's two way communications channel to the web
38server, so our FCGI module can't be used as the text management front
39end. This is probably a good idea to keep them seperate anyway, for
40security, coz the web server is exposed to the world, the console isn't.
41
42Currently sledjchisl.c tests to see if it's running in tmux already, if
43it isn't it starts up tmux runs itself into this new tmux, then exits.
44So it could also test if it's running from FCGI, and switch to web mode,
45then it'll need to find the tmuxed instance to send commands to it.
46Either via nails connection, or sending tmux commands via shell.
47
48FCGI has methods of dealing with auth and templates. B-)
49
50So for now I think I'll have the text and web management front ends in
51sledjchisl.c, and the love world server as well. I can split them up
52later if I need to.
53
54
55--------------------------------------------------------------------
56
57How to install it.
58------------------
59
60It's all still partly written, un-released, and experimental at the
61moment. So thes are just rough notes about what is needed.
62
63There are two parts, the OpenSim runner part and the web pages part. The
64OpenSim runner part requjires the rest of opensim-SC to be installed,
65which is covered by other documents in this source code repo. SO the
66below only talks about the web pages part.
67
68So far I have only tried this with Apache 2, but it should work fine with
69other web servers that support FCGI. I'm using spawn-fcgi which was
70written for lighttpd, and seems to be the proper way to support FCGI in
71Nginx as well.
72
73Until I have actually released this, it'll be source code only. So you
74need a C development environment to compile all the C source code.
75
76Some of the dependencies are included, like LuaJIT, qLibc, the FCGI SDK,
77and toybox. Or at least their source code git ropes are cloned during
78the build stage. The other dependencies are the development environments
79for MariaDB or MySQL (only tested with MariaDB), OpenSSL, and UUID. And
80spawn-fcgi. In a Debian based Linux distro, that could be installed by
81something like -
82
83apt install libmariadbclient-dev libssl1.0-dev uuid-dev spawn-fcgi
84
85Once you have all of that, you can run the src/BuildIt.sh script to put
86it all together. That script will also actually run the web side of
87things. Often during this early development process, that script will
88run things under a test tool like valgrind, so you'll probably need that
89to.
90
91You'll need to configure your web server to pass web requests onto
92sledjchisl. I'll add instructions for other popular web servers later,
93but for now, this is what to do with Apache 2.
94
95Load the mod_proxy_fcgi module. Include something like this in your
96virtual host definition -
97
98<FilesMatch "\.fcgi$">
99 SetHandler "proxy:unix:///opt/opensim_SC/var/cache/sledjchisl.socket|fcgi://localhost/"
100</FilesMatch>
101
102Adjust that unix:// path to match if you have installed things elsewhere.
103Also make that directory readable by the web server group.
104
105Copy the files in example/www to where eveqer your web servers document
106root is.
107
108Note that the account.html dynamic web page redirects to a HTTPS version
109of itself, so you'll need HTTPS to be working.
110
111The current web pages will then be available at something like -
112
113http://localhost/sledjchisl.fcgi/loginpage.html
114
115http://localhost/sledjchisl.fcgi/stats.html
116
117https://localhost/sledjchisl.fcgi/account.html
118
diff --git a/src/sledjchisl/fcgi_SC.c b/src/sledjchisl/fcgi_SC.c
new file mode 100644
index 0000000..36aba77
--- /dev/null
+++ b/src/sledjchisl/fcgi_SC.c
@@ -0,0 +1,13 @@
1/* fcgi_SC.h - Generic fcgi handler, coz the others all suck.
2 *
3 * Copyright 2020 David Seikel <onefang@sledjhamr.org>
4 */
5
6// I use camelCaseNames internally, instead of underscore_names as is preferred
7// in the rest of toybox. A small limit of 80 characters per source line infers
8// shorter names should be used. CamelCaseNames are shorter. Externally visible
9// stuff is underscore_names as usual. Plus, I'm used to camelCaseNames, my
10// fingers twitch that way.
11
12#include "toys.h"
13#include "fcgi_SC.h"
diff --git a/src/sledjchisl/fcgi_SC.h b/src/sledjchisl/fcgi_SC.h
new file mode 100644
index 0000000..2b3fa65
--- /dev/null
+++ b/src/sledjchisl/fcgi_SC.h
@@ -0,0 +1,136 @@
1/* fcgi_SC.h - Generic fcgi handler, coz the oters all suck.
2 *
3 * Copyright 2020 David Seikel <onefang@sledjhamr.org>
4 */
5
6enum fcgiEventType{
7 FSC_CSI,
8 FSC_KEYS,
9 FSC_MOUSE,
10 FSC_RAW
11};
12
13struct fcgiEvent {
14 enum fcgiEventType type; // The type of this event.
15 char *sequence; // Either a translated sequence, or raw bytes.
16 int isTranslated; // Whether or not sequence is translated.
17 int count; // Number of entries in params.
18 int *params; // For CSI events, the decoded parameters.
19};
20
21
22
23
24// From the spec.
25
26/*
27 * Listening socket file number
28 */
29#define FCGI_LISTENSOCK_FILENO 0
30
31typedef struct {
32 unsigned char version;
33 unsigned char type;
34 unsigned char requestIdB1;
35 unsigned char requestIdB0;
36 unsigned char contentLengthB1;
37 unsigned char contentLengthB0;
38 unsigned char paddingLength;
39 unsigned char reserved;
40} FCGI_Header;
41
42/*
43 * Number of bytes in a FCGI_Header. Future versions of the protocol
44 * will not reduce this number.
45 */
46#define FCGI_HEADER_LEN 8
47
48/*
49 * Value for version component of FCGI_Header
50 */
51#define FCGI_VERSION_1 1
52
53/*
54 * Values for type component of FCGI_Header
55 */
56#define FCGI_BEGIN_REQUEST 1
57#define FCGI_ABORT_REQUEST 2
58#define FCGI_END_REQUEST 3
59#define FCGI_PARAMS 4
60#define FCGI_STDIN 5
61#define FCGI_STDOUT 6
62#define FCGI_STDERR 7
63#define FCGI_DATA 8
64#define FCGI_GET_VALUES 9
65#define FCGI_GET_VALUES_RESULT 10
66#define FCGI_UNKNOWN_TYPE 11
67#define FCGI_MAXTYPE (FCGI_UNKNOWN_TYPE)
68
69/*
70 * Value for requestId component of FCGI_Header
71 */
72#define FCGI_NULL_REQUEST_ID 0
73
74typedef struct {
75 unsigned char roleB1;
76 unsigned char roleB0;
77 unsigned char flags;
78 unsigned char reserved[5];
79} FCGI_BeginRequestBody;
80
81typedef struct {
82 FCGI_Header header;
83 FCGI_BeginRequestBody body;
84} FCGI_BeginRequestRecord;
85
86/*
87 * Mask for flags component of FCGI_BeginRequestBody
88 */
89#define FCGI_KEEP_CONN 1
90
91/*
92 * Values for role component of FCGI_BeginRequestBody
93 */
94#define FCGI_RESPONDER 1
95#define FCGI_AUTHORIZER 2
96#define FCGI_FILTER 3
97
98typedef struct {
99 unsigned char appStatusB3;
100 unsigned char appStatusB2;
101 unsigned char appStatusB1;
102 unsigned char appStatusB0;
103 unsigned char protocolStatus;
104 unsigned char reserved[3];
105} FCGI_EndRequestBody;
106
107typedef struct {
108 FCGI_Header header;
109 FCGI_EndRequestBody body;
110} FCGI_EndRequestRecord;
111
112/*
113 * Values for protocolStatus component of FCGI_EndRequestBody
114 */
115#define FCGI_REQUEST_COMPLETE 0
116#define FCGI_CANT_MPX_CONN 1
117#define FCGI_OVERLOADED 2
118#define FCGI_UNKNOWN_ROLE 3
119
120/*
121 * Variable names for FCGI_GET_VALUES / FCGI_GET_VALUES_RESULT records
122 */
123#define FCGI_MAX_CONNS "FCGI_MAX_CONNS"
124#define FCGI_MAX_REQS "FCGI_MAX_REQS"
125#define FCGI_MPXS_CONNS "FCGI_MPXS_CONNS"
126
127typedef struct {
128 unsigned char type;
129 unsigned char reserved[7];
130} FCGI_UnknownTypeBody;
131
132typedef struct {
133 FCGI_Header header;
134 FCGI_UnknownTypeBody body;
135} FCGI_UnknownTypeRecord;
136
diff --git a/src/sledjchisl/script.lua b/src/sledjchisl/script.lua
new file mode 100644
index 0000000..1e4b909
--- /dev/null
+++ b/src/sledjchisl/script.lua
@@ -0,0 +1,18 @@
1-- script.lua
2
3-- This works coz LuaJIT automatically loads the jit module.
4if type(jit) == 'table' then
5 io.write('script.lua is being run by ' .. jit.version .. ' under ' .. jit.os .. ' on a ' .. jit.arch .. '\n')
6else
7 io.write('script.lua is being run by Lua version ' .. _VERSION .. '\n')
8end
9
10-- Receives a table, returns the sum of its components.
11io.write("The table the script received has:\n");
12x = 0
13for i = 1, #foo do
14 print(i, foo[i])
15 x = x + foo[i]
16end
17io.write("Returning data back to C\n");
18return x
diff --git a/src/sledjchisl/sledjchisl.c b/src/sledjchisl/sledjchisl.c
new file mode 100644
index 0000000..42ed205
--- /dev/null
+++ b/src/sledjchisl/sledjchisl.c
@@ -0,0 +1,7342 @@
1/* sledjchisl.c - opensim-SC management system.
2 *
3 * Copyright 2020 David Seikel <sledjchisl@sledjhamr.org>
4 * Not in SUSv4. An entirely new invention, thus no web site either.
5
6USE_SLEDJCHISL(NEWTOY(sledjchisl, "m(mode):", TOYFLAG_USR|TOYFLAG_BIN))
7
8config SLEDJCHISL
9 bool "sledjchisl"
10 default y
11 help
12 usage: sledjchisl [-m|--mode mode]
13
14 opensim-SC management system.
15*/
16
17
18// TODO - figure out how to automate testing of this.
19// Being all interactive and involving external web servers / viewers makes it hard.
20
21// TODO - do all the setup on first run, and check if needed on each start up, to be self healing.
22
23// TODO - pepper could be entered on the console on startup if it's not defined, as a master password sort of thing.
24// I'd go as far as protecting the database credentials that way, but legacy OpenSim needs it unprotected.
25// Also keep in mind, people want autostart of their services without having to enter passwords on each boot.
26
27// TODO - once it is event driven, periodically run things like session clean ups, self healing, and the secure.sh thing.
28// And backups off course.
29// As well as regular database pings to keep the connection open.
30
31#include <fcgi_config.h>
32#ifdef _WIN32
33#include <process.h>
34#else
35extern char **environ;
36#endif
37// Don't overide standard stdio stuff.
38#define NO_FCGI_DEFINES
39#include <fcgi_stdio.h>
40#undef NO_FCGI_DEFINES
41//#include "fcgiapp.h"
42
43#include <lua.h>
44#include <lualib.h>
45#include <lauxlib.h>
46#include <luajit.h>
47
48#include "lib/fcgi_SC.h"
49#include "lib/handlekeys.h"
50
51// Both my_config.h and fcgi_config.h define the same PACKAGE* variables, which we don't use anyway,
52// I deal with that by using a sed invokation when building fcgi2.
53
54// https://mariadb.com/kb/en/about-mariadb-connector-c/ Official docs.
55// http://dev.mysql.com/doc/refman/5.5/en/c-api-function-overview.html MySQL docs.
56// http://zetcode.com/db/mysqlc/ MySQL tutorial.
57#include <my_global.h>
58#include <mysql.h>
59
60#include <qlibc.h>
61#include <extensions/qconfig.h>
62
63// TODO - I should probably replace openSSL with something else. Only using it for the hash functions, and apparently it's got a bit of a bad rep.
64// qLibc optionally uses openSSL for it's HTTP client stuff.
65#include <openssl/crypto.h>
66#include <openssl/evp.h>
67#include "openssl/hmac.h"
68#include <uuid/uuid.h>
69
70// Toybox's strend overrides another strend that causes MariaDB library to crash. Renaming it to tb_strend helps.
71// I deal with that by using a sed invokation when building toybox.
72#include "toys.h"
73
74
75GLOBALS(
76 char *mode;
77)
78
79#define TT this.sledjchisl
80
81#define FLAG_m 2
82
83
84
85// Duplicate some small amount of code from qLibc, coz /, + and, = are not good choices, and the standard says we can pick those.
86/**
87 * Encode data using BASE64 algorithm.
88 *
89 * @param bin a pointer of input data.
90 * @param size the length of input data.
91 *
92 * @return a malloced string pointer of BASE64 encoded string in case of
93 * successful, otherwise returns NULL
94 *
95 * @code
96 * const char *text = "hello world";
97 *
98 * char *encstr = qB64_encode(text, strlen(text));
99 * if(encstr == NULL) return -1;
100 *
101 * printf("Original: %s\n", text);
102 * printf("Encoded : %s\n", encstr);
103 *
104 * size_t decsize = qB64_decode(encstr);
105 *
106 * printf("Decoded : %s (%zu bytes)\n", encstr, decsize);
107 * free(encstr);
108 *
109 * --[output]--
110 * Original: hello world
111 * Encoded: aGVsbG8gd29ybGQ=
112 * Decoded: hello world (11 bytes)
113 * @endcode
114 */
115char *qB64_encode(const void *bin, size_t size) {
116 const char B64CHARTBL[64] = {
117 'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P', // 00-0F
118 'Q','R','S','T','U','V','W','X','Y','Z','a','b','c','d','e','f', // 10-1F
119 'g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v', // 20-2F
120 'w','x','y','z','0','1','2','3','4','5','6','7','8','9','+','/' // 30-3F
121 };
122
123 if (size == 0) {
124 return strdup("");
125 }
126
127 // malloc for encoded string
128 char *pszB64 = (char *) malloc(
129 4 * ((size / 3) + ((size % 3 == 0) ? 0 : 1)) + 1);
130 if (pszB64 == NULL) {
131 return NULL;
132 }
133
134 char *pszB64Pt = pszB64;
135 unsigned char *pBinPt, *pBinEnd = (unsigned char *) (bin + size - 1);
136 unsigned char szIn[3] = { 0, 0, 0 };
137 int nOffset;
138 for (pBinPt = (unsigned char *) bin, nOffset = 0; pBinPt <= pBinEnd;
139 pBinPt++, nOffset++) {
140 int nIdxOfThree = nOffset % 3;
141 szIn[nIdxOfThree] = *pBinPt;
142 if (nIdxOfThree < 2 && pBinPt < pBinEnd)
143 continue;
144
145 *pszB64Pt++ = B64CHARTBL[((szIn[0] & 0xFC) >> 2)];
146 *pszB64Pt++ = B64CHARTBL[(((szIn[0] & 0x03) << 4)
147 | ((szIn[1] & 0xF0) >> 4))];
148 *pszB64Pt++ =
149 (nIdxOfThree >= 1) ?
150 B64CHARTBL[(((szIn[1] & 0x0F) << 2)
151 | ((szIn[2] & 0xC0) >> 6))] :
152 '=';
153 *pszB64Pt++ = (nIdxOfThree >= 2) ? B64CHARTBL[(szIn[2] & 0x3F)] : '=';
154
155 memset((void *) szIn, 0, sizeof(szIn));
156 }
157 *pszB64Pt = '\0';
158
159 pszB64 = qstrreplace("tr", pszB64, "+", "~");
160 pszB64 = qstrreplace("tr", pszB64, "/", "_");
161 pszB64 = qstrreplace("tr", pszB64, "=", "^");
162
163 return pszB64;
164}
165
166/**
167 * Decode BASE64 encoded string.
168 *
169 * @param str a pointer of Base64 encoded string.
170 *
171 * @return the length of bytes stored in the str memory in case of successful,
172 * otherwise returns NULL
173 *
174 * @note
175 * This modify str directly. And the 'str' is always terminated by NULL
176 * character.
177 */
178size_t qB64_decode(char *str) {
179 const char B64MAPTBL[16 * 16] = {
180 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, // 00-0F
181 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, // 10-1F
182 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 62, 64, 64, 64, 63, // 20-2F
183 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 64, 64, 64, 64, 64, 64, // 30-3F
184 64, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, // 40-4F
185 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 64, 64, 64, 64, 64, // 50-5F
186 64, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, // 60-6F
187 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 64, 64, 64, 64, 64, // 70-7F
188 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, // 80-8F
189 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, // 90-9F
190 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, // A0-AF
191 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, // B0-BF
192 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, // C0-CF
193 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, // D0-DF
194 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, // E0-EF
195 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64 // F0-FF
196 };
197
198 str = qstrreplace("tr", str, "~", "+");
199 str = qstrreplace("tr", str, "_", "/");
200 str = qstrreplace("tr", str, "^", "=");
201
202 char *pEncPt, *pBinPt = str;
203 int nIdxOfFour = 0;
204 char cLastByte = 0;
205 for (pEncPt = str; *pEncPt != '\0'; pEncPt++) {
206 char cByte = B64MAPTBL[(unsigned char) (*pEncPt)];
207 if (cByte == 64)
208 continue;
209
210 if (nIdxOfFour == 0) {
211 nIdxOfFour++;
212 } else if (nIdxOfFour == 1) {
213 // 00876543 0021????
214 //*pBinPt++ = ( ((cLastByte << 2) & 0xFC) | ((cByte >> 4) & 0x03) );
215 *pBinPt++ = ((cLastByte << 2) | (cByte >> 4));
216 nIdxOfFour++;
217 } else if (nIdxOfFour == 2) {
218 // 00??8765 004321??
219 //*pBinPt++ = ( ((cLastByte << 4) & 0xF0) | ((cByte >> 2) & 0x0F) );
220 *pBinPt++ = ((cLastByte << 4) | (cByte >> 2));
221 nIdxOfFour++;
222 } else {
223 // 00????87 00654321
224 //*pBinPt++ = ( ((cLastByte << 6) & 0xC0) | (cByte & 0x3F) );
225 *pBinPt++ = ((cLastByte << 6) | cByte);
226 nIdxOfFour = 0;
227 }
228
229 cLastByte = cByte;
230 }
231 *pBinPt = '\0';
232
233 return (pBinPt - str);
234}
235
236
237// Duplicate some small amount of code from toys/pending/sh.c
238// TODO - to be really useful I need to return the output.
239int runToy(char *argv[])
240{
241 int ret = 0;
242 struct toy_list *tl;
243 struct toy_context temp;
244 sigjmp_buf rebound;
245
246 if ((tl = toy_find(argv[0])) )//&& (tl->flags & (TOYFLAG_NOFORK|TOYFLAG_MAYFORK)))
247 {
248 // This fakes lots of what toybox_main() does.
249 memcpy(&temp, &toys, sizeof(struct toy_context));
250 memset(&toys, 0, sizeof(struct toy_context));
251
252 if (!sigsetjmp(rebound, 1))
253 {
254 toys.rebound = &rebound;
255 toy_init(tl, argv); // argv must be null terminated
256 tl->toy_main();
257 xflush(0);
258 }
259 ret = toys.exitval;
260 if (toys.optargs != toys.argv+1) free(toys.optargs);
261 if (toys.old_umask) umask(toys.old_umask);
262 memcpy(&toys, &temp, sizeof(struct toy_context));
263 }
264
265 return ret;
266}
267
268
269#undef FALSE
270#undef TRUE
271#ifndef FALSE
272// NEVER change this, true and false work to.
273typedef enum
274{
275 FALSE = 0,
276 TRUE = 1
277} boolean;
278#endif
279
280
281
282// Silly "man getrandom" is bullshitting.
283// Note - this is Linux specific, it's calling a Linux kernel function.
284// Remove this when we have a real getrandom(), and replace it with -
285// #include <sys/random.h>
286#include <sys/syscall.h>
287#include <linux/random.h>
288int getrandom(void *b, size_t l, unsigned int f)
289{
290 return (int) syscall(SYS_getrandom, b, l, f);
291}
292
293
294
295typedef struct _gridStats gridStats;
296struct _gridStats
297{
298 float next;
299 struct timeval last;
300 qhashtbl_t *stats;
301};
302
303
304typedef struct _HTMLfile HTMLfile;
305struct _HTMLfile
306{
307 struct timespec last;
308 qlist_t *fragments;
309};
310qhashtbl_t *HTMLfileCache = NULL;
311
312
313typedef struct _reqData reqData;
314
315typedef void *(*pageFunction) (char *file, reqData *Rd, HTMLfile *thisFile);
316typedef struct _dynPage dynPage;
317struct _dynPage
318{
319 char *name;
320 pageFunction func;
321};
322qhashtbl_t *dynPages;
323static void newDynPage(char *name, pageFunction func)
324{
325 dynPage *dp = xmalloc(sizeof(dynPage));
326 dp->name = name; dp->func = func;
327 dynPages->put(dynPages, dp->name, dp, sizeof(dynPage));
328 free(dp);
329}
330
331
332#define HMACSIZE EVP_MAX_MD_SIZE * 2
333#define HMACSIZE64 88
334
335// Session details about the logged in web user. A sorta state machine. Ish.
336enum reqSessionStatus // Status of the session. Refresh and wipe / nuke -> delete the old session file first.
337{
338 SHS_UNKNOWN = 0, // Haven't looked at the session yet. -> validate it
339 SHS_NONE, // No session at all. -> logout
340 SHS_BOGUS, // Looked at the session, it's bogus. -> nuke and logout
341 SHS_PROBLEM, // Some other problem with the session. -> nuke and logout
342 SHS_VALID, // Session is valid. -> continue
343
344 SHS_LOGIN, // User has just logged in, add UUID to session. -> wipe, add UUID
345
346 SHS_RENEW, // Refresh the session based on timer. -> continue
347 SHS_REFRESH, // Refresh the session for other reason. -> continue
348 SHS_IDLE, // Session has been idle too long. -> relogin
349 SHS_ANCIENT, // Session is way too old. -> nuke and logout
350
351 SHS_SECURITY, // High security task needs users name & password. ->
352 SHS_RELOGIN, // Ask user to login again. ->
353
354 SHS_KEEP, // Keep the session. -> continue
355 SHS_WIPE, // Wipe the session, use users UUID. -> continue
356 SHS_NUKE // Wipe the session, no UUID. -> logout
357};
358
359typedef struct _sesh sesh;
360struct _sesh
361{
362 char salt[256 + 1], seshID[256 + 1],
363 sesh[256 + 16 + 10 + 1], munchie[HMACSIZE + 16 + 10 + 1], toke_n_munchie[HMACSIZE + 1], hashish[HMACSIZE + 1],
364 leaf[HMACSIZE64 + 6 + 1], *UUID, *name;
365 struct timespec timeStamp[2];
366 short level;
367 enum reqSessionStatus status;
368 boolean isLinky;
369};
370
371// Details about the current web request.
372struct _reqData
373{
374 lua_State *L;
375 qhashtbl_t *configs, *queries, *body, *cookies, *headers, *valid, *stuff, *database, *Rcookies, *Rheaders;
376 char *Scheme, *Host, *Method, *Script, *Path, *RUri, *doit, *form, *output, *outQuery;
377 sesh shs, *lnk;
378 gridStats *stats;
379 qlist_t *errors, *messages;
380 qgrow_t *reply;
381 struct timespec then;
382 boolean fromDb;
383};
384
385static void showSesh(qgrow_t *reply, sesh *shs)
386{
387 if (shs->isLinky)
388 reply->addstrf(reply, "Linky:<br>\n<pre>\n");
389 else
390 reply->addstrf(reply, "Session:<br>\n<pre>\n");
391
392 if (NULL != shs->name)
393 reply->addstrf(reply, " &nbsp; name = %s\n", shs->name);
394 if (NULL != shs->UUID)
395 reply->addstrf(reply, " &nbsp; UUID = %s\n", shs->UUID);
396 reply->addstrf(reply, " &nbsp; salt = %s\n", shs->salt);
397 reply->addstrf(reply, " &nbsp; seshID = %s\n", shs->seshID);
398 reply->addstrf(reply, " &nbsp; timeStamp = %ld.%ld\n", shs->timeStamp[1].tv_sec, shs->timeStamp[1].tv_nsec);
399 reply->addstrf(reply, " &nbsp; sesh = %s\n", shs->sesh);
400 reply->addstrf(reply, " &nbsp; munchie = %s\n", shs->munchie);
401 reply->addstrf(reply, " &nbsp; toke_n_munchie = %s\n", shs->toke_n_munchie);
402 reply->addstrf(reply, " &nbsp; hashish = %s\n", shs->hashish);
403 reply->addstrf(reply, " &nbsp; leaf = %s\n", shs->leaf);
404 reply->addstrf(reply, " &nbsp; level = %d\n", (int) shs->level);
405 reply->addstr(reply, "</pre>\n");
406}
407
408
409char toybuf[4096];
410lua_State *L;
411qhashtbl_t *configs;
412MYSQL *database, *dbconn;
413unsigned int dbTimeout;
414struct timespec dbLast;
415my_bool dbReconnect;
416gridStats *stats;
417boolean isTmux = 0;
418boolean isWeb = 0;
419char *pwd = "";
420char *scRoot = "/opt/opensim_SC";
421char *scUser = "opensimsc";
422char *scBin = "";
423char *scEtc = "";
424char *scLib = "";
425char *scRun = "";
426char *scBackup = "";
427char *scCache = "";
428char *scData = "";
429char *scLog = "";
430char *Tconsole = "SledjChisl";
431char *Tsocket = "opensim-tmux.socket";
432char *Ttab = "SC";
433char *Tcmd = "tmux -S";
434char *webRoot = "/var/www/html";
435char *URL = "fcgi-bin/sledjchisl.fcgi";
436char *ToS = "Be good.";
437char *webIframers = "";
438int seshRenew = 10 * 60;
439int idleTimeOut = 30 * 60;
440int seshTimeOut = 24 * 60 * 60;
441int newbieTimeOut = 30;
442float loadAverageInc = 0.5;
443int simTimeOut = 45;
444boolean DEBUG = TRUE;
445qhashtbl_t *mimeTypes;
446qlist_t *dbRequests;
447
448
449// TODO - log to file. The problem is we don't know where to log until after we have loaded the configs, and before that we are spewing log messages.
450// Now that we are using spawn-fcgi, all the logs are going to STDERR, which we can capture and write to a file.
451// Unfortunately spawn-fcgi in deamon mode sends all the output to /dev/null or something.
452// A better idea, when we spawn tmux or spawn-fcgi, capture STDERR, full log everything to that, filtered log to the tmux console (STDOUT).
453// Then we can use STDOUT / STDIN to run the console stuff.
454
455// TODO - escape anything that will turn the console into garbage.
456
457// https://stackoverflow.com/questions/4842424/list-of-ansi-color-escape-sequences
458char *logTypes[] =
459{
460 "91;1;4", "CRITICAL", // red underlined
461 "31", "ERROR", // dark red
462 "93", "WARNING", // yellow
463 "36", "TIMEOUT", // cyan
464 "97;40", "INFO", // white
465 "90", "DEBUG", // grey
466// VERBOSE? UNKNOWN? FATAL? SILENT? All from Android aparently.
467 "35", "debug", // magenta
468 "34", "timeout", // blue
469};
470
471#define DATE_TIME_LEN 42
472void logMe(int v, char *format, ...)
473{
474 va_list va, va2;
475 int len;
476 char *ret;
477 struct timeval tv;
478 time_t curtime;
479 char date[DATE_TIME_LEN];
480
481 va_start(va, format);
482 va_copy(va2, va);
483 // How long is it?
484 len = vsnprintf(0, 0, format, va);
485 len++;
486 va_end(va);
487 // Allocate and do the sprintf()
488 ret = xmalloc(len);
489 vsnprintf(ret, len, format, va2);
490 va_end(va2);
491
492 gettimeofday(&tv, NULL);
493 curtime = tv.tv_sec;
494 strftime(date, DATE_TIME_LEN, "(%Z %z) %F %T", localtime(&curtime));
495
496 v *= 2;
497 fprintf(stderr, "%s.%.6ld \e[%sm%-8s sledjchisl: %s\e[0m\n", date, tv.tv_usec, logTypes[v], logTypes[v + 1], ret);
498 free(ret);
499}
500#define C(...) logMe(0, __VA_ARGS__)
501#define E(...) logMe(1, __VA_ARGS__)
502#define W(...) logMe(2, __VA_ARGS__)
503#define T(...) logMe(3, __VA_ARGS__)
504#define I(...) logMe(4, __VA_ARGS__)
505#define D(...) logMe(5, __VA_ARGS__)
506#define d(...) logMe(6, __VA_ARGS__)
507#define t(...) logMe(7, __VA_ARGS__)
508
509
510static void addStrL(qlist_t *list, char *s)
511{
512 list->addlast(list, s, strlen(s) + 1);
513}
514
515static char *getStrH(qhashtbl_t *hash, char *key)
516{
517 char *ret = "", *t;
518
519 t = hash->getstr(hash, key, false);
520 if (NULL != t)
521 ret = t;
522 return ret;
523}
524
525
526char *myHMAC(char *in, boolean b64)
527{
528 EVP_MD_CTX *mdctx = EVP_MD_CTX_create(); // Gets renamed to EVP_MD_CTX_new() in later versions.
529 unsigned char md_value[EVP_MAX_MD_SIZE];
530 unsigned int md_len;
531
532 EVP_DigestInit_ex(mdctx, EVP_sha512(), NULL); // EVP_sha3_512() isn't available until later versions.
533 EVP_DigestUpdate(mdctx, in, strlen(in));
534 EVP_DigestFinal_ex(mdctx, md_value, &md_len);
535 EVP_MD_CTX_destroy(mdctx); // Gets renamed to EVP_MD_CTX_free() in later versions.
536
537 if (b64)
538 return qB64_encode(md_value, md_len);
539 else
540 return qhex_encode(md_value, md_len);
541}
542
543char *myHMACkey(char *key, char *in, boolean b64)
544{
545 unsigned char md_value[EVP_MAX_MD_SIZE];
546 unsigned int md_len;
547 unsigned char* digest = HMAC(EVP_sha512(), key, strlen(key), (unsigned char *) in, strlen(in), md_value, &md_len);
548
549 if (b64)
550 return qB64_encode(md_value, md_len);
551 else
552 return qhex_encode(md_value, md_len);
553}
554
555
556// In Lua 5.0 reference manual is a table traversal example at page 29.
557void PrintTable(lua_State *L)
558{
559 lua_pushnil(L);
560
561 while (lua_next(L, -2) != 0)
562 {
563 // Numbers can convert to strings, so check for numbers before checking for strings.
564 if (lua_isnumber(L, -1))
565 printf("%s = %f\n", lua_tostring(L, -2), lua_tonumber(L, -1));
566 else if (lua_isstring(L, -1))
567 printf("%s = '%s'\n", lua_tostring(L, -2), lua_tostring(L, -1));
568 else if (lua_istable(L, -1))
569 PrintTable(L);
570 lua_pop(L, 1);
571 }
572}
573
574
575int sendTmuxKeys(char *dest, char *keys)
576{
577 int ret = 0, i;
578 char *c = xmprintf("%s %s/%s send-keys -t %s:%s '%s'", Tcmd, scRun, Tsocket, Tconsole, dest, keys);
579
580 i = system(c);
581 if (!WIFEXITED(i))
582 E("tmux send-keys command failed!");
583 free(c);
584 return ret;
585}
586
587int sendTmuxCmd(char *dest, char *cmd)
588{
589 int ret = 0, i;
590 char *c = xmprintf("%s %s/%s send-keys -t %s:'%s' '%s' Enter", Tcmd, scRun, Tsocket, Tconsole, dest, cmd);
591
592 i = system(c);
593 if (!WIFEXITED(i))
594 E("tmux send-keys command failed!");
595 free(c);
596 return ret;
597}
598
599void waitTmuxText(char *dest, char *text)
600{
601 int i;
602 // Using " for the grep pattern, coz ' might be used in a sim name.
603// TODO - should escape \ " ` in text.
604 char *c = xmprintf("sleep 5; %s %s/%s capture-pane -t %s:'%s' -p | grep -F \"%s\" 2>&1 > /dev/null", Tcmd, scRun, Tsocket, Tconsole, dest, text);
605
606 D("Waiting for '%s'.", text);
607 do
608 {
609 i = system(c);
610 if (!WIFEXITED(i))
611 {
612 E("tmux capture-pane command failed!");
613 break;
614 }
615 else if (0 == WEXITSTATUS(i))
616 break;
617 } while (1);
618
619 free(c);
620}
621
622float waitLoadAverage(float la, float extra, int timeout)
623{
624 struct sysinfo info;
625 struct timespec timeOut;
626 float l;
627 int to = timeout;
628
629 T("Sleeping until load average is below %.02f (%.02f + %.02f) or for %d seconds.", la + extra, la, extra, timeout);
630 clock_gettime(CLOCK_MONOTONIC, &timeOut);
631 to += timeOut.tv_sec;
632
633 do
634 {
635 msleep(5000);
636 sysinfo(&info);
637 l = info.loads[0]/65536.0;
638 clock_gettime(CLOCK_MONOTONIC, &timeOut);
639 timeout -= 5;
640 t("Tick, load average is %.02f, countdown %d seconds.", l, timeout);
641 } while (((la + extra) < l) && (timeOut.tv_sec < to));
642
643 return l;
644}
645
646
647// Rob forget to do this, but at least he didn't declare it static.
648struct dirtree *dirtree_handle_callback(struct dirtree *new, int (*callback)(struct dirtree *node));
649
650typedef struct _simList simList;
651struct _simList
652{
653 int len, num;
654 char **sims;
655};
656
657static int filterSims(struct dirtree *node)
658{
659 if (!node->parent) return DIRTREE_RECURSE | DIRTREE_SHUTUP;
660 if ((strncmp(node->name, "sim", 3) == 0) && ((strcmp(node->name, "sim_skeleton") != 0)))
661 {
662 simList *list = (simList *) node->parent->extra;
663
664 if ((list->num + 1) > list->len)
665 {
666 list->len = list->len + 1;
667 list->sims = xrealloc(list->sims, list->len * sizeof(char *));
668 }
669 list->sims[list->num] = xstrdup(node->name);
670 list->num++;
671 }
672 return 0;
673}
674
675// We particularly don't want \ " `
676char *cleanSimName(char *name)
677{
678 size_t l = strlen(name);
679 char *ret = xmalloc(l + 1);
680 int i, j = 0;
681
682 for (i = 0; i < l; i++)
683 {
684 char r = name[i];
685
686 if ((' ' == r) || (isalnum(r) != 0))
687 ret[j++] = r;
688 }
689 ret[j] = '\0';
690
691 return ret;
692}
693
694simList *getSims()
695{
696 simList *sims = xmalloc(sizeof(simList));
697 memset(sims, 0, sizeof(simList));
698 char *path = xmprintf("%s/config", scRoot);
699 struct dirtree *new = dirtree_add_node(0, path, 0);
700 new->extra = (long) sims;
701 dirtree_handle_callback(new, filterSims);
702
703 qsort(sims->sims, sims->num, sizeof(char *), qstrcmp);
704 free(path);
705 return sims;
706}
707
708void freeSimList(simList *sims)
709{
710 int i;
711
712 for (i = 0; i < sims->num; i++)
713 free(sims->sims[i]);
714 free(sims->sims);
715 free(sims);
716}
717
718static int filterInis(struct dirtree *node)
719{
720 if (!node->parent) return DIRTREE_RECURSE | DIRTREE_SHUTUP;
721 int l = strlen(node->name);
722 if (strncmp(&(node->name[l - 4]), ".ini", 4) == 0)
723 {
724 strcpy((char *) node->parent->extra, node->name);
725 return DIRTREE_ABORT;
726 }
727 return 0;
728}
729
730char *getSimName(char *sim)
731{
732 char *ret = NULL;
733 char *c = xmprintf("%s/config/%s", scRoot, sim);
734 struct dirtree *new = dirtree_add_node(0, c, 0);
735
736 free(c);
737 c = xzalloc(1024);
738 new->extra = (long) c;
739 dirtree_handle_callback(new, filterInis);
740 if ('\0' != c[0])
741 {
742 char *temp = NULL;
743 regex_t pat;
744 regmatch_t m[2];
745 long len;
746 int fd;
747
748 temp = xmprintf("%s/config/%s/%s", scRoot, sim, c);
749 fd = xopenro(temp);
750 xregcomp(&pat, "RegionName = \"(.+)\"", REG_EXTENDED);
751 do
752 {
753 // TODO - get_line() is slow, and wont help much with DOS and Mac line endings.
754 // gio_gets() isn't any faster really, but deals with DOS line endings at least.
755 free(temp);
756 temp = get_line(fd);
757 if (temp)
758 {
759 if (!regexec(&pat, temp, 2, m, 0))
760 {
761 // Return first parenthesized subexpression as string.
762 if (pat.re_nsub > 0)
763 {
764 ret = xmprintf("%.*s", (int) (m[1].rm_eo - m[1].rm_so), temp + m[1].rm_so);
765 free(temp);
766 break;
767 }
768 }
769 }
770 } while (temp);
771 regfree(&pat);
772 xclose(fd);
773 }
774 free(c);
775 return ret;
776}
777
778
779// Expects either "simXX" or "ROBUST".
780int checkSimIsRunning(char *sim)
781{
782 int ret = 0;
783 struct stat st;
784
785 // Check if it's running.
786 char *path = xmprintf("%s/caches/%s.pid", scRoot, sim);
787 if (0 == stat(path, &st))
788 {
789 int fd, i;
790 char *pid = NULL;
791
792 // Double check if it's REALLY running.
793 if ((fd = xopenro(path)) == -1)
794 perror_msg("xopenro(%s)", path);
795 else
796 {
797 pid = get_line(fd);
798 if (NULL == pid)
799 perror_msg("get_line(%s)", path);
800 else
801 {
802 xclose(fd);
803
804// I'd rather re-use the toysh command running stuff, since ps is a toy, but that's private.
805// TODO - switch to toybox ps and rm.
806 free(path);
807 path = xmprintf("ps -p %s --no-headers -o comm", pid);
808 i = system(path);
809 if (WIFEXITED(i))
810 {
811 if (0 != WEXITSTATUS(i)) // No such pid.
812 {
813 free(path);
814 path = xmprintf("rm -f %s/caches/%s.pid", scRoot, sim);
815 d("%s", path);
816 i = system(path);
817 }
818 else
819 d("checkSimIsRunning(%s) has PID %s, which is actually running.", sim, pid);
820 }
821 }
822 }
823 free(pid);
824 }
825
826 // Now check if it's really really running. lol
827 free(path);
828 path = xmprintf("%s/caches/%s.pid", scRoot, sim);
829 if (0 == stat(path, &st))
830 {
831 D("checkSimIsRunning(%s) -> %s is really really running.", sim, path);
832 ret = 1;
833 }
834 else
835 D("checkSimIsRunning(%s) -> %s is not running.", sim, path);
836
837 free(path);
838 return ret;
839}
840
841
842static void PrintEnv(qgrow_t *reply, char *label, char **envp)
843{
844 reply->addstrf(reply, "%s:<br>\n<pre>\n", label);
845 for ( ; *envp != NULL; envp++)
846 reply->addstrf(reply, "%s\n", *envp);
847 reply->addstr(reply, "</pre>\n");
848}
849
850static void printEnv(char **envp)
851{
852 for ( ; *envp != NULL; envp++)
853 D("%s", *envp);
854}
855
856
857typedef struct _rowData rowData;
858struct _rowData
859{
860 char **fieldNames;
861 qlist_t *rows;
862};
863
864static void dumpHash(qhashtbl_t *tbl)
865{
866 qhashtbl_obj_t obj;
867
868 memset((void*)&obj, 0, sizeof(obj));
869 tbl->lock(tbl);
870 while(tbl->getnext(tbl, &obj, true) == true)
871 d("%s = %s", obj.name, (char *) obj.data);
872 tbl->unlock(tbl);
873}
874
875static void dumpArray(int d, char **ar)
876{
877 int i = 0;
878
879 while (ar[i] != NULL)
880 {
881 d("%d %d %s", d, i, ar[i]);
882 i++;
883 }
884}
885
886
887
888typedef struct _dbFields dbFields;
889struct _dbFields
890{
891 qlisttbl_t *flds;
892 int count;
893};
894typedef struct _dbField dbField;
895struct _dbField
896{
897 char *name;
898 enum enum_field_types type;
899 unsigned long length;
900 unsigned int flags;
901 unsigned int decimals;
902};
903
904void dbFreeFields(dbFields *flds, boolean all)
905{
906 flds->count--;
907
908// TODO - sigh, looks to be inconsistant why some do and some don't leak.
909// I guess the ones that don't leak are the ones that crash?
910// It's only a tiny leak anyway, 80 bytes total.
911// if ((0 >= flds->count) || all) // CRASHY
912 if ((0 >= flds->count)) // LEAKY
913 {
914 qlisttbl_obj_t obj;
915
916 memset((void *) &obj, 0, sizeof(obj));
917 flds->flds->lock(flds->flds);
918 while(flds->flds->getnext(flds->flds, &obj, NULL, false) == true)
919 {
920 dbField *fld = (dbField *) obj.data;
921 free(fld->name);
922 }
923 flds->flds->unlock(flds->flds);
924 flds->flds->free(flds->flds);
925 flds->flds = NULL;
926 free(flds);
927 }
928}
929
930enum dbCommandType
931{
932 CT_SELECT,
933 CT_CREATE,
934 CT_UPDATE,
935 CT_NONE
936};
937
938typedef struct _dbRequest dbRequest;
939struct _dbRequest
940{
941 char *table, *join, *where, *order, *sql;
942 MYSQL_STMT *prep; // NOTE - executing it stores state in this.
943 dbFields *fields;
944 int inCount, outCount, rowCount;
945 char **inParams, **outParams;
946 MYSQL_BIND *inBind, *outBind;
947 rowData *rows;
948 my_ulonglong count;
949 enum dbCommandType type;
950 boolean freeOutParams;
951};
952
953void dbFreeRequest(dbRequest *req, boolean all)
954{
955 int i;
956
957 D("Cleaning up prepared database request %s - %s %d %d", req->table, req->where, req->outCount, req->inCount);
958
959 if (NULL != req->outBind)
960 {
961 for (i = 0; i < req->outCount; i++)
962 {
963 if (NULL != req->outBind[i].buffer) free(req->outBind[i].buffer);
964 if (NULL != req->outBind[i].length) free(req->outBind[i].length);
965 if (NULL != req->outBind[i].error) free(req->outBind[i].error);
966 if (NULL != req->outBind[i].is_null) free(req->outBind[i].is_null);
967 }
968 free(req->outBind);
969 req->outBind = NULL;
970 }
971 else
972 D(" No out binds to clean up for %s - %s.", req->table, req->where);
973 if (NULL != req->inBind)
974 {
975 for (i = 0; i < req->inCount; i++)
976 {
977 if (NULL != req->inBind[i].buffer) free(req->inBind[i].buffer);
978 if (NULL != req->inBind[i].length) free(req->inBind[i].length);
979 if (NULL != req->inBind[i].error) free(req->inBind[i].error);
980 if (NULL != req->inBind[i].is_null) free(req->inBind[i].is_null);
981 }
982 free(req->inBind);
983 req->inBind = NULL;
984 }
985 else
986 D(" No in binds to clean up for %s - %s.", req->table, req->where);
987
988 if (req->freeOutParams && all)
989 {
990 if (NULL != req->outParams)
991 {
992 free(req->outParams);
993 req->outParams = NULL;
994 }
995 else
996 D(" No out params to clean up for %s - %s.", req->table, req->where);
997 }
998 if (NULL != req->sql) free(req->sql);
999 else
1000 D(" No SQL to clean up for %s - %s.", req->table, req->where);
1001 req->sql = NULL;
1002 if (NULL != req->prep)
1003 {
1004 if (0 != mysql_stmt_close(req->prep))
1005 C(" Unable to close the prepared statement!");
1006 req->prep = NULL;
1007 }
1008
1009 if (all)
1010 {
1011 if (NULL != req->fields)
1012 {
1013 dbFreeFields(req->fields, all);
1014 req->fields = NULL;
1015 }
1016 else
1017 D(" No fields to clean up for %s - %s.", req->table, req->where);
1018 }
1019}
1020
1021void freeDb(boolean all)
1022{
1023 dbRequest **rq;
1024
1025 if (dbRequests)
1026 {
1027 if (all)
1028 {
1029 while (NULL != (rq = (dbRequest **) dbRequests->popfirst(dbRequests, NULL)))
1030 {
1031 dbFreeRequest(*rq, all);
1032 free(rq);
1033 }
1034 dbRequests->free(dbRequests);
1035 dbRequests = NULL;
1036 }
1037 else
1038 {
1039 qlist_obj_t obj;
1040
1041 memset((void*)&obj, 0, sizeof(obj)); // must be cleared before call
1042 dbRequests->lock(dbRequests);
1043 while (dbRequests->getnext(dbRequests, &obj, false) == true)
1044 dbFreeRequest(*((dbRequest **) obj.data), all);
1045 dbRequests->unlock(dbRequests);
1046 }
1047 }
1048
1049 if (database) mysql_close(database);
1050 database = NULL;
1051 mysql_library_end();
1052}
1053
1054static boolean dbConnect()
1055{
1056 database = mysql_init(NULL);
1057 if (NULL == database)
1058 {
1059 E("mysql_init() failed - %s", mysql_error(database));
1060 return FALSE;
1061 }
1062
1063/* TODO - dammit, no mysql_get_option(), MariaDB docs say mysql_get_optionv(), which doesn't exist either.
1064 Says "This function was added in MariaDB Connector/C 3.0.0.", I have MariaDB / MySQL client version: 10.1.44-MariaDB.
1065
1066 if (mysql_get_option(database, MYSQL_OPT_CONNECT_TIMEOUT, &dbTimeout))
1067 E("mysql_get_option(MYSQL_OPT_CONNECT_TIMEOUT) failed - %s", mysql_error(database));
1068 else
1069 D("Database MYSQL_OPT_CONNECT_TIMEOUT = %d", dbTimeout);
1070
1071 if (mysql_get_option(database, MYSQL_OPT_RECONNECT, &dbReconnect))
1072 E("mysql_get_option(MYSQL_OPT_RECONNECT) failed - %s", mysql_error(database));
1073 else
1074 D("Database MYSQL_OPT_RECONNECT = %d", (int) dbReconnect);
1075*/
1076
1077 // Seems best to disable auto-reconnect, so I have more control over reconnections.
1078 dbReconnect = 0;
1079 if (mysql_options(database, MYSQL_OPT_RECONNECT, &dbReconnect))
1080 E("mysql_options(MYSQL_OPT_RECONNECT) failed - %s", mysql_error(database));
1081 else
1082 D("Database MYSQL_OPT_RECONNECT is now %d", (int) dbReconnect);
1083
1084 dbconn = mysql_real_connect(database,
1085 getStrH(configs, "Data Source"),
1086 getStrH(configs, "User ID"),
1087 getStrH(configs, "Password"),
1088 getStrH(configs, "Database"),
1089// 3036, "/var/run/mysqld/mysqld.sock",
1090 0, NULL,
1091 CLIENT_FOUND_ROWS | CLIENT_LOCAL_FILES | CLIENT_MULTI_STATEMENTS | CLIENT_MULTI_RESULTS);
1092 if (NULL == dbconn)
1093 {
1094 E("mysql_real_connect() failed - %s", mysql_error(database));
1095 return FALSE;
1096 }
1097
1098 // Just set the fucking thing to a year. Pffft.
1099 dbTimeout = 60 * 60 * 24 * 7 * 52;
1100 char *sql = xmprintf("SET SESSION wait_timeout=%d", (int) dbTimeout);
1101
1102 if (mysql_query(database, sql))
1103 E("SET SESSION wait_timeout=%d failed - %s", (int) dbTimeout, mysql_error(database));
1104 else
1105 D("Database wait_timeout = %d", (int) dbTimeout);
1106 free(sql);
1107
1108 if (-1 == clock_gettime(CLOCK_REALTIME, &dbLast))
1109 perror_msg("Unable to get the time.");
1110
1111 return TRUE;
1112}
1113
1114// A general error function that checks for certain errors that mean we should try to connect to the server MariaDB again.
1115// https://mariadb.com/kb/en/mariadb-error-codes/
1116// 1129? 1152? 1184? 1218? 1927 3032? 4150?
1117// "server has gone away" isn't listed there, that's the one I was getting. Pffft
1118// It's 2006, https://dev.mysql.com/doc/refman/8.0/en/gone-away.html
1119// Ah it could be "connection inactive for 8 hours".
1120// Which might be why OpenSim opens a new connection for EVERYTHING.
1121// https://dev.mysql.com/doc/refman/5.7/en/c-api-auto-reconnect.html
1122// Has more details.
1123static boolean dbCheckError(char *error, char *sql)
1124{
1125 int e = mysql_errno(database);
1126
1127 E("MariaDB error %d - %s: %s\n%s", e, error, mysql_error(database), sql);
1128 if (2006 == e)
1129 {
1130 W("Reconnecting to database.");
1131 freeDb(false);
1132 return dbConnect();
1133 }
1134
1135 return FALSE;
1136}
1137// "Statement execute failed 2013: Lost connection to MySQL server during query"
1138static boolean dbStmtCheckError(dbRequest *req, char *error, char *sql)
1139{
1140 int e = mysql_stmt_errno(req->prep);
1141
1142 E("MariaDB prepared statement error %d - %s: %s\n%s", e, error, mysql_stmt_error(req->prep), sql);
1143 if (2013 == e)
1144 {
1145 W("Reconnecting to database.");
1146 freeDb(false);
1147 return dbConnect();
1148 }
1149
1150 return FALSE;
1151}
1152
1153dbFields *dbGetFields(char *table)
1154{
1155 static qhashtbl_t *tables = NULL;
1156 if (NULL == tables) tables = qhashtbl(0, 0);
1157 dbFields *ret = tables->get(tables, table, NULL, false);
1158
1159 if (NULL == ret)
1160 {
1161 // Seems the only way to get field metadata is to actually perform a SQL statement, then you get the field metadata for the result set.
1162 // Chicken, meet egg, sorry you had to cross the road for this.
1163 char *sql = xmprintf("SELECT * FROM %s LIMIT 0", table);
1164
1165d("Getting field metadata for %s", table);
1166 if (mysql_query(database, sql))
1167 {
1168// E("MariaDB error %d - Query failed 0: %s\n%s", mysql_errno(database), mysql_error(database), sql);
1169 if (dbCheckError("Query failed 0", sql))
1170 {
1171 ret = dbGetFields(table);
1172 free(sql);
1173 return ret;
1174 }
1175 }
1176 else
1177 {
1178 MYSQL_RES *res = mysql_store_result(database);
1179
1180 if (!res)
1181 E("MariaDB error %d - Couldn't get results set from %s\n %s", mysql_errno(database), mysql_error(database), sql);
1182 else
1183 {
1184 MYSQL_FIELD *fields = mysql_fetch_fields(res);
1185
1186 if (!fields)
1187 E("MariaDB error %d - Failed fetching fields: %s", mysql_errno(database), mysql_error(database));
1188 else
1189 {
1190 unsigned int i, num_fields = mysql_num_fields(res);
1191
1192 ret = xmalloc(sizeof(dbFields)); // Little bit LEAKY
1193 ret->flds = qlisttbl(QLISTTBL_UNIQUE | QLISTTBL_LOOKUPFORWARD);
1194 ret->count = 1;
1195 for (i = 0; i < num_fields; i++)
1196 {
1197 dbField *fld = xmalloc(sizeof(dbField));
1198 fld->name = xstrdup(fields[i].name);
1199 fld->type = fields[i].type;
1200 fld->length = fields[i].length;
1201 fld->flags = fields[i].flags;
1202 fld->decimals = fields[i].decimals;
1203 ret->flds->put(ret->flds, fld->name, fld, sizeof(*fld));
1204 free(fld);
1205 }
1206 tables->put(tables, table, ret, sizeof(*ret));
1207 }
1208 mysql_free_result(res);
1209 }
1210 }
1211 free(sql);
1212 }
1213 else // Reference count these, coz some tables are used more than once.
1214 ret->count++;
1215
1216 return ret;
1217}
1218
1219
1220/* How to deal with prepared SQL statements.
1221http://karlssonondatabases.blogspot.com/2010/07/prepared-statements-are-they-useful-or.html
1222https://blog.cotten.io/a-taste-of-mysql-in-c-87c5de84a31d?gi=ab3dd1425b29
1223https://raspberry-projects.com/pi/programming-in-c/databases-programming-in-c/mysql/accessing-the-database
1224
1225IG and CG now both have sims connected to other grids, so some sort of
1226multi database solution would be good, then we can run the grid and the
1227external sims all in one.
1228
1229Not sure if this'll work with Count(*).
1230
1231---------------------------------------------
1232
1233The complicated bit is the binds.
1234
1235You are binding field values to C memory locations.
1236The parameters and returned fields need binds.
1237Mostly seems to be the value parts of the SQL statements.
1238
1239I suspect most will be of the form -
1240 ... WHERE x=? and foo=?
1241 INSERT INTO table VALUES (?,?,?)
1242 UPDATE table SET x=?, foo=? WHERE id=?
1243
1244 A multi table update -
1245 UPDATE items,month SET items.price=month.price WHERE items.id=month.id;
1246*/
1247
1248int dbDoSomething(dbRequest *req, boolean count, ...)
1249{
1250 int ret = 0;
1251 va_list ap;
1252 struct timespec then;
1253 int i, j;
1254 MYSQL_RES *prepare_meta_result = NULL;
1255
1256 if (-1 == clock_gettime(CLOCK_REALTIME, &then))
1257 perror_msg("Unable to get the time.");
1258
1259// TODO - should factor this out to it's own function, and call that function in dbCount() and dbCountJoin().
1260// Or better yet, finally migrate those functions to using dbDoSomething().
1261 double n = (dbLast.tv_sec * 1000000000.0) + dbLast.tv_nsec;
1262 double t = (then.tv_sec * 1000000000.0) + then.tv_nsec;
1263
1264t("Database timeout test %lf > %lf", ((t - n) / 1000000000.0), (dbTimeout / 2.0));
1265 if (((t - n) / 1000000000.0) > (dbTimeout / 2.0))
1266 {
1267 T("Avoid database timeout of %d seconds, pinging it.", dbTimeout);
1268 if (0 != mysql_ping(database))
1269 {
1270 W("Reconnecting to database.");
1271 freeDb(false);
1272 dbConnect();
1273 }
1274 }
1275
1276 va_start(ap, count);
1277
1278 if (NULL == req->prep)
1279 {
1280 D("Creating prepared statement for %s - %s", req->table, req->where);
1281
1282 if (0 == req->type)
1283 req->type = CT_SELECT;
1284
1285 req->fields = dbGetFields(req->table);
1286 if (NULL == req->fields)
1287 {
1288 E("Unknown fields for table %s.", req->table);
1289 ret++;
1290 goto end;
1291 }
1292
1293 switch (req->type)
1294 {
1295 case CT_SELECT :
1296 {
1297 char *select = xmprintf("");
1298
1299 i = 0;
1300 while (req->outParams[i] != NULL)
1301 {
1302 char *t = xmprintf("%s,%s", select, req->outParams[i]);
1303 free(select);
1304 select = t;
1305 i++;
1306 }
1307 if (0 == i)
1308 {
1309 free(select);
1310 if (count)
1311 select = xmprintf(",Count(*)");
1312 else
1313 select = xmprintf(",*");
1314 }
1315
1316 if (NULL == req->join)
1317 req->join = "";
1318
1319 if (req->where)
1320 req->sql = xmprintf("SELECT %s FROM %s %s WHERE %s", &select[1], req->table, req->join, req->where);
1321 else
1322 req->sql = xmprintf("SELECT %s FROM %s", &select[1], req->table, req->join);
1323 free(select);
1324 if (req->order)
1325 {
1326 char *t = xmprintf("%s ORDER BY %s", req->sql, req->order);
1327
1328 free(req->sql);
1329 req->sql = t;
1330 }
1331 break;
1332 }
1333
1334 case CT_CREATE :
1335 {
1336 char *values = xmprintf("");
1337
1338 i = 0;
1339 while (req->inParams[i] != NULL)
1340 {
1341 char *t = xmprintf("%s, %s=?", values, req->inParams[i]);
1342 free(values);
1343 values = t;
1344 i++;
1345 }
1346 if (0 == i)
1347 {
1348 E("Statement prepare for INSERT must have in paramaters.");
1349 ret++;
1350 free(values);
1351 values = xmprintf("");
1352 }
1353 req->sql = xmprintf("INSERT INTO %s SET %s", req->table, &values[1]);
1354 free(values);
1355
1356 break;
1357 }
1358
1359 case CT_UPDATE :
1360 {
1361 break;
1362 }
1363
1364 case CT_NONE :
1365 {
1366 W("No SQL type!");
1367 break;
1368 }
1369 }
1370
1371d("New SQL statement - %s", req->sql);
1372 // prepare statement with the other fields
1373 req->prep = mysql_stmt_init(database);
1374 if (NULL == req->prep)
1375 {
1376 E("Statement prepare init failed: %s\n", mysql_stmt_error(req->prep));
1377 ret++;
1378 goto end;
1379 }
1380 if (mysql_stmt_prepare(req->prep, req->sql, strlen(req->sql)))
1381 {
1382 E("Statement prepare failed: %s\n", mysql_stmt_error(req->prep));
1383 ret++;
1384 goto end;
1385 }
1386
1387 // setup the bind stuff for any "?" parameters in the SQL.
1388 req->inCount = mysql_stmt_param_count(req->prep);
1389 i = 0;
1390 while (req->inParams[i] != NULL)
1391 i++;
1392 if (i != req->inCount)
1393 {
1394 E("In parameters count don't match %d != %d for - %s", i, req->inCount, req->sql);
1395 ret++;
1396 goto freeIt;
1397 }
1398 req->inBind = xzalloc(i * sizeof(MYSQL_BIND));
1399//W("Allocated %d %d inBinds for %s", i, req->inCount, req->sql);
1400 for (i = 0; i < req->inCount; i++)
1401 {
1402 dbField *fld = req->fields->flds->get(req->fields->flds, req->inParams[i], NULL, false);
1403
1404 if (NULL == fld)
1405 {
1406 E("Unknown input field %d %s.%s for - %s", i, req->table, req->inParams[i], req->sql);
1407 ret++;
1408 goto freeIt;
1409 }
1410 else
1411 {
1412 // https://blog.cotten.io/a-taste-of-mysql-in-c-87c5de84a31d?gi=ab3dd1425b29
1413 // For some gotchas about all of this binding bit.
1414 req->inBind[i].buffer_type = fld->type;
1415 req->inBind[i].buffer = xzalloc(fld->length + 1); // Note the + 1 is for string types, and a waste for the rest.
1416 req->inBind[i].buffer_length = fld->length + 1;
1417 switch(fld->type)
1418 {
1419 case MYSQL_TYPE_TINY:
1420 {
1421//d("TINY %d %s %d", i, fld->name, req->inBind[i].buffer_length);
1422 break;
1423 }
1424
1425 case MYSQL_TYPE_SHORT:
1426 {
1427 req->inBind[i].is_unsigned = FALSE;
1428//d("SHORT %d %s %d", i, fld->name, req->inBind[i].buffer_length);
1429 break;
1430 }
1431
1432 case MYSQL_TYPE_INT24:
1433 {
1434 req->inBind[i].is_unsigned = FALSE;
1435//d("INT24 %d %s %d", i, fld->name, req->inBind[i].buffer_length);
1436 break;
1437 }
1438
1439 case MYSQL_TYPE_LONG:
1440 {
1441 req->inBind[i].is_unsigned = FALSE;
1442//d("LONG %d %s %d", i, fld->name, req->inBind[i].buffer_length);
1443 break;
1444 }
1445
1446 case MYSQL_TYPE_LONGLONG:
1447 {
1448 req->inBind[i].is_unsigned = FALSE;
1449//d("LONGLONG %d %s %d", i, fld->name, req->inBind[i].buffer_length);
1450 break;
1451 }
1452
1453 case MYSQL_TYPE_FLOAT:
1454 {
1455//d("FLOAT %d %s %d", i, fld->name, req->inBind[i].buffer_length);
1456 break;
1457 }
1458
1459 case MYSQL_TYPE_DOUBLE:
1460 {
1461//d("DOUBLE %d %s %d", i, fld->name, req->inBind[i].buffer_length);
1462 break;
1463 }
1464
1465 case MYSQL_TYPE_NEWDECIMAL:
1466 {
1467//d("NEWDECIMAL %d %s %d", i, fld->name, req->inBind[i].buffer_length);
1468 break;
1469 }
1470
1471 case MYSQL_TYPE_TIME:
1472 case MYSQL_TYPE_DATE:
1473 case MYSQL_TYPE_DATETIME:
1474 case MYSQL_TYPE_TIMESTAMP:
1475 {
1476//d("DATE / TIME ish %d %s %d", i, fld->name, req->inBind[i].buffer_length);
1477 break;
1478 }
1479
1480 case MYSQL_TYPE_STRING:
1481 case MYSQL_TYPE_VAR_STRING:
1482 {
1483//d("STRING / VARSTRING %d %s %d", i, fld->name, req->inBind[i].buffer_length);
1484 req->inBind[i].is_null = xzalloc(sizeof(my_bool));
1485 req->inBind[i].length = xzalloc(sizeof(unsigned long));
1486 break;
1487 }
1488
1489 case MYSQL_TYPE_TINY_BLOB:
1490 case MYSQL_TYPE_BLOB:
1491 case MYSQL_TYPE_MEDIUM_BLOB:
1492 case MYSQL_TYPE_LONG_BLOB:
1493 {
1494//d("BLOBs %d %s %d", i, fld->name, req->inBind[i].buffer_length);
1495 req->inBind[i].is_null = xzalloc(sizeof(my_bool));
1496 break;
1497 }
1498
1499 case MYSQL_TYPE_BIT:
1500 {
1501 req->inBind[i].is_null = xzalloc(sizeof(my_bool));
1502//d("BIT %d %s %d", i, fld->name, req->inBind[i].buffer_length);
1503 break;
1504 }
1505
1506 case MYSQL_TYPE_NULL:
1507 {
1508//d("NULL %d %s %d", i, fld->name, req->inBind[i].buffer_length);
1509 break;
1510 }
1511 }
1512 }
1513 }
1514
1515// TODO - if this is not a count, setup result bind paramateres, may be needed for counts as well.
1516 if (CT_SELECT == req->type)
1517 {
1518 prepare_meta_result = mysql_stmt_result_metadata(req->prep);
1519 if (!prepare_meta_result)
1520 {
1521 E(" mysql_stmt_result_metadata() error %d, returned no meta information - %s\n", mysql_stmt_errno(req->prep), mysql_stmt_error(req->prep));
1522 ret++;
1523 goto freeIt;
1524 }
1525 }
1526
1527 if (count)
1528 {
1529I("count!!!!!!!!!!!!!!!!");
1530 }
1531 else if (CT_SELECT == req->type)
1532 {
1533 req->outCount = mysql_num_fields(prepare_meta_result);
1534 i = 0;
1535 while (req->outParams[i] != NULL)
1536 i++;
1537 if (0 == i) // Passing in {NULL} as req->outParams means "return all of them".
1538 {
1539 req->outParams = xzalloc((req->outCount + 1) * sizeof(char *));
1540 req->freeOutParams = TRUE;
1541 qlisttbl_obj_t obj;
1542 memset((void*)&obj, 0, sizeof(obj));
1543 req->fields->flds->lock(req->fields->flds);
1544 while (req->fields->flds->getnext(req->fields->flds, &obj, NULL, false) == true)
1545 {
1546 dbField *fld = (dbField *) obj.data;
1547 req->outParams[i] = fld->name;
1548 i++;
1549 }
1550 req->outParams[i] = NULL;
1551 req->fields->flds->unlock(req->fields->flds);
1552 }
1553 if (i != req->outCount)
1554 {
1555 E("Out parameters count doesn't match %d != %d foqr - %s", i, req->outCount, req->sql);
1556 ret++;
1557 goto freeIt;
1558 }
1559 req->outBind = xzalloc(i * sizeof(MYSQL_BIND));
1560//W("Allocated %d %d outBinds for %s", i, req->outCount, req->sql);
1561 for (i = 0; i < req->outCount; i++)
1562 {
1563 dbField *fld = req->fields->flds->get(req->fields->flds, req->outParams[i], NULL, false);
1564
1565 if (NULL == fld)
1566 {
1567 E("Unknown output field %d %s.%s foqr - %s", i, req->table, req->outParams[i], req->sql);
1568 ret++;
1569 goto freeIt;
1570 }
1571 else
1572 {
1573 // https://blog.cotten.io/a-taste-of-mysql-in-c-87c5de84a31d?gi=ab3dd1425b29
1574 // For some gotchas about all of this binding bit.
1575 req->outBind[i].buffer_type = fld->type;
1576 req->outBind[i].buffer = xzalloc(fld->length + 1); // Note the + 1 is for string types, and a waste for the rest.
1577 req->outBind[i].buffer_length = fld->length + 1;
1578 req->outBind[i].error = xzalloc(sizeof(my_bool));
1579 req->outBind[i].is_null = xzalloc(sizeof(my_bool));
1580 switch(fld->type)
1581 {
1582 case MYSQL_TYPE_TINY:
1583 {
1584//d("TINY %d %s %d", i, fld->name, req->outBind[i].buffer_length);
1585 break;
1586 }
1587
1588 case MYSQL_TYPE_SHORT:
1589 {
1590//d("SHORT %s %d", fld->name, req->outBind[i].buffer_length);
1591 req->outBind[i].is_unsigned = FALSE;
1592 break;
1593 }
1594
1595 case MYSQL_TYPE_INT24:
1596 {
1597//d("INT24 %s %d", fld->name, req->outBind[i].buffer_length);
1598 req->outBind[i].is_unsigned = FALSE;
1599 break;
1600 }
1601
1602 case MYSQL_TYPE_LONG:
1603 {
1604//d("LONG %d %s %d", i, fld->name, req->outBind[i].buffer_length);
1605 req->outBind[i].is_unsigned = FALSE;
1606 break;
1607 }
1608
1609 case MYSQL_TYPE_LONGLONG:
1610 {
1611//d("LONGLONG %s %d", fld->name, req->outBind[i].buffer_length);
1612 req->outBind[i].is_unsigned = FALSE;
1613 break;
1614 }
1615
1616 case MYSQL_TYPE_FLOAT:
1617 {
1618//d("FLOAT %s %d", fld->name, req->outBind[i].buffer_length);
1619 break;
1620 }
1621
1622 case MYSQL_TYPE_DOUBLE:
1623 {
1624//d("DOUBLE %s %d", fld->name, req->outBind[i].buffer_length);
1625 break;
1626 }
1627
1628 case MYSQL_TYPE_NEWDECIMAL:
1629 {
1630//d("NEWDECIMAL %s %d", fld->name, req->outBind[i].buffer_length);
1631 break;
1632 }
1633
1634 case MYSQL_TYPE_TIME:
1635 case MYSQL_TYPE_DATE:
1636 case MYSQL_TYPE_DATETIME:
1637 case MYSQL_TYPE_TIMESTAMP:
1638 {
1639//d("DATE / TIME ish %s %d", fld->name, req->outBind[i].buffer_length);
1640 break;
1641 }
1642
1643 case MYSQL_TYPE_STRING:
1644 case MYSQL_TYPE_VAR_STRING:
1645 {
1646//d("STRING / VARSTRING %s %d", fld->name, req->outBind[i].buffer_length);
1647 req->outBind[i].length = xzalloc(sizeof(unsigned long));
1648 break;
1649 }
1650
1651 case MYSQL_TYPE_TINY_BLOB:
1652 case MYSQL_TYPE_BLOB:
1653 case MYSQL_TYPE_MEDIUM_BLOB:
1654 case MYSQL_TYPE_LONG_BLOB:
1655 {
1656//d("BLOBs %s %d", fld->name, req->outBind[i].buffer_length);
1657 break;
1658 }
1659
1660 case MYSQL_TYPE_BIT:
1661 {
1662//d("BIT %s %d", fld->name, req->outBind[i].buffer_length);
1663 break;
1664 }
1665
1666 case MYSQL_TYPE_NULL:
1667 {
1668//d("NULL %s %d", fld->name, req->outBind[i].buffer_length);
1669 break;
1670 }
1671 }
1672 }
1673 }
1674 if (mysql_stmt_bind_result(req->prep, req->outBind))
1675 {
1676 E("Bind failed error %d.", mysql_stmt_errno(req->prep));
1677 ret++;
1678 goto freeIt;
1679 }
1680 }
1681 }
1682
1683
1684//d("input bind for %s", req->sql);
1685 for (i = 0; i < req->inCount; i++)
1686 {
1687 dbField *fld = req->fields->flds->get(req->fields->flds, req->inParams[i], NULL, false);
1688
1689 if (NULL == fld)
1690 {
1691 E("Unknown input field %s.%s for - %s", req->table, req->inParams[i], req->sql);
1692 ret++;
1693 goto freeIt;
1694 }
1695 else
1696 {
1697 switch(fld->type)
1698 {
1699 case MYSQL_TYPE_TINY:
1700 {
1701 int c = va_arg(ap, int);
1702 signed char d = (signed char) c;
1703
1704 memcpy(req->inBind[i].buffer, &d, (size_t) fld->length);
1705//T("TINY %d %s %d", i, fld->name, req->inBind[i].buffer_length);
1706 break;
1707 }
1708
1709 case MYSQL_TYPE_SHORT:
1710 {
1711 int c = va_arg(ap, int);
1712 short int d = (short int) c;
1713
1714 memcpy(req->inBind[i].buffer, &d, (size_t) fld->length);
1715//T("SHORT %d %s %d = %d", i, fld->name, req->inBind[i].buffer_length, c);
1716 break;
1717 }
1718
1719 case MYSQL_TYPE_INT24:
1720 {
1721 int d = va_arg(ap, int);
1722
1723 memcpy(req->inBind[i].buffer, &d, (size_t) fld->length);
1724//T("INT24 %d %s %d - %d", i, fld->name, req->inBind[i].buffer_length, d);
1725 break;
1726 }
1727
1728 case MYSQL_TYPE_LONG:
1729 {
1730 long d = va_arg(ap, long);
1731
1732 memcpy(req->inBind[i].buffer, &d, (size_t) fld->length);
1733//T("LONG %d %s %d = %ld", i, fld->name, req->inBind[i].buffer_length, d);
1734 break;
1735 }
1736
1737 case MYSQL_TYPE_LONGLONG:
1738 {
1739 long long int d = va_arg(ap, long long int);
1740
1741 memcpy(req->inBind[i].buffer, &d, (size_t) fld->length);
1742//T("LONGLONG %d %s %d = %lld", i, fld->name, req->inBind[i].buffer_length, d);
1743 break;
1744 }
1745
1746 case MYSQL_TYPE_FLOAT:
1747 {
1748 double c = va_arg(ap, double);
1749 float d = (float) c;
1750
1751 memcpy(req->inBind[i].buffer, &d, (size_t) fld->length);
1752//T("FLOAT %d %s %d = %f", i, fld->name, req->inBind[i].buffer_length, d);
1753 break;
1754 }
1755
1756 case MYSQL_TYPE_DOUBLE:
1757 {
1758 double d = va_arg(ap, double);
1759
1760 memcpy(req->inBind[i].buffer, &d, (size_t) fld->length);
1761//T("DOUBLE %d %s %d = %f", i, fld->name, req->inBind[i].buffer_length, d);
1762 break;
1763 }
1764
1765 case MYSQL_TYPE_NEWDECIMAL:
1766 {
1767//T("NEWDECIMAL %d %s %d", i, fld->name, req->inBind[i].buffer_length);
1768 break;
1769 }
1770
1771 case MYSQL_TYPE_TIME:
1772 case MYSQL_TYPE_DATE:
1773 case MYSQL_TYPE_DATETIME:
1774 case MYSQL_TYPE_TIMESTAMP:
1775 {
1776 MYSQL_TIME d = va_arg(ap, MYSQL_TIME);
1777
1778 memcpy(req->inBind[i].buffer, &d, (size_t) fld->length);
1779//T("DATE / TIME ish %d %s %d", i, fld->name, req->inBind[i].buffer_length);
1780 break;
1781 }
1782
1783 case MYSQL_TYPE_STRING:
1784 case MYSQL_TYPE_VAR_STRING:
1785 {
1786 char *d = va_arg(ap, char *);
1787 unsigned long l = strlen(d);
1788
1789 if (l > fld->length)
1790 l = fld->length;
1791 *(req->inBind[i].length) = l;
1792 strncpy(req->inBind[i].buffer, d, (size_t) l);
1793 ((char *) req->inBind[i].buffer)[l] = '\0';
1794//T("STRING / VARSTRING %d %s %d = %s", i, fld->name, req->inBind[i].buffer_length, d);
1795 break;
1796 }
1797
1798 case MYSQL_TYPE_TINY_BLOB:
1799 case MYSQL_TYPE_BLOB:
1800 case MYSQL_TYPE_MEDIUM_BLOB:
1801 case MYSQL_TYPE_LONG_BLOB:
1802 {
1803// TODO - should write this, we will likely need it. Main problem is - how long is this blob? Probably should add a length param before the blob.
1804//T("BLOBs %d %s %d", i, fld->name, req->inBind[i].buffer_length);
1805 break;
1806 }
1807
1808 case MYSQL_TYPE_BIT:
1809 {
1810//T("BIT %d %s %d", i, fld->name, req->inBind[i].buffer_length);
1811 break;
1812 }
1813
1814 case MYSQL_TYPE_NULL:
1815 {
1816//T("NULL %d %s %d", i, fld->name, req->inBind[i].buffer_length);
1817 break;
1818 }
1819 }
1820 }
1821 }
1822 if (mysql_stmt_bind_param(req->prep, req->inBind))
1823 {
1824 E("Bind failed error %d.", mysql_stmt_errno(req->prep));
1825 ret++;
1826 goto freeIt;
1827 }
1828
1829
1830//d("Execute %s", req->sql);
1831
1832 // do the prepared statement req->prep.
1833 if (mysql_stmt_execute(req->prep))
1834 {
1835 if (dbStmtCheckError(req, "Statement failed 0", req->sql))
1836 {
1837 ret++;
1838 goto freeIt;
1839 }
1840 }
1841
1842 int fs = mysql_stmt_field_count(req->prep);
1843 // stuff results back into req.
1844 if (NULL != req->outBind)
1845 {
1846 req->rows = xmalloc(sizeof(rowData));
1847 req->rows->fieldNames = xzalloc(fs * sizeof(char *));
1848 if (mysql_stmt_store_result(req->prep))
1849 {
1850 E(" mysql_stmt_store_result() failed %d: %s", mysql_stmt_errno(req->prep), mysql_stmt_error(req->prep));
1851 ret++;
1852 goto freeIt;
1853 }
1854 req->rowCount = mysql_stmt_num_rows(req->prep);
1855 if (0 == req->rowCount)
1856 D("No rows returned from : %s\n", req->sql);
1857 else
1858 D("%d rows of %d fields returned from : %s\n", req->rowCount, fs, req->sql);
1859
1860 req->rows->rows = qlist(0);
1861 while (MYSQL_NO_DATA != mysql_stmt_fetch(req->prep))
1862 {
1863 qhashtbl_t *flds = qhashtbl(0, 0);
1864
1865 for (i = 0; i < req->outCount; i++)
1866 {
1867 dbField *fld = req->fields->flds->get(req->fields->flds, req->outParams[i], NULL, false);
1868
1869 req->rows->fieldNames[i] = fld->name;
1870 if (!*(req->outBind[i].is_null))
1871 {
1872//d("2.8 %s", req->rows->fieldNames[i]);
1873 flds->put(flds, req->rows->fieldNames[i], req->outBind[i].buffer, req->outBind[i].buffer_length);
1874
1875 switch(fld->type)
1876 {
1877 case MYSQL_TYPE_TINY:
1878 {
1879 break;
1880 }
1881
1882 case MYSQL_TYPE_SHORT:
1883 {
1884 char *t = xmprintf("%d", (int) *((int *) req->outBind[i].buffer));
1885 flds->putstr(flds, req->rows->fieldNames[i], t);
1886 free(t);
1887 break;
1888 }
1889
1890 case MYSQL_TYPE_INT24:
1891 {
1892 char *t = xmprintf("%d", (int) *((int *) req->outBind[i].buffer));
1893 flds->putstr(flds, req->rows->fieldNames[i], t);
1894 free(t);
1895 break;
1896 }
1897
1898 case MYSQL_TYPE_LONG:
1899 {
1900 if (NULL == req->outBind[i].buffer)
1901 {
1902 E("Field %d %s is NULL", i, fld->name);
1903 ret++;
1904 goto freeIt;
1905 }
1906 char *t = xmprintf("%d", (int) *((int *) (req->outBind[i].buffer)));
1907//d("Setting %i %s %s", i, fld->name, t);
1908 flds->putstr(flds, req->rows->fieldNames[i], t);
1909 free(t);
1910 break;
1911 }
1912
1913 case MYSQL_TYPE_LONGLONG:
1914 {
1915 char *t = xmprintf("%d", (int) *((int *) req->outBind[i].buffer));
1916 flds->putstr(flds, req->rows->fieldNames[i], t);
1917 free(t);
1918 break;
1919 }
1920
1921 case MYSQL_TYPE_FLOAT:
1922 {
1923 break;
1924 }
1925
1926 case MYSQL_TYPE_DOUBLE:
1927 {
1928 break;
1929 }
1930
1931 case MYSQL_TYPE_NEWDECIMAL:
1932 {
1933 break;
1934 }
1935
1936 case MYSQL_TYPE_TIME:
1937 case MYSQL_TYPE_DATE:
1938 case MYSQL_TYPE_DATETIME:
1939 case MYSQL_TYPE_TIMESTAMP:
1940 {
1941 break;
1942 }
1943
1944 case MYSQL_TYPE_STRING:
1945 case MYSQL_TYPE_VAR_STRING:
1946 {
1947 break;
1948 }
1949
1950 case MYSQL_TYPE_TINY_BLOB:
1951 case MYSQL_TYPE_BLOB:
1952 case MYSQL_TYPE_MEDIUM_BLOB:
1953 case MYSQL_TYPE_LONG_BLOB:
1954 {
1955 break;
1956 }
1957
1958 case MYSQL_TYPE_BIT:
1959 {
1960 break;
1961 }
1962
1963 case MYSQL_TYPE_NULL:
1964 {
1965 break;
1966 }
1967 }
1968 }
1969 else
1970 D("Not setting data %s, coz it's NULL", fld->name);
1971 }
1972 req->rows->rows->addlast(req->rows->rows, flds, sizeof(qhashtbl_t));
1973 free(flds);
1974 }
1975 }
1976
1977freeIt:
1978 if (prepare_meta_result)
1979 mysql_free_result(prepare_meta_result);
1980 if (mysql_stmt_free_result(req->prep))
1981 {
1982 E("Statement result freeing failed %d: %s\n", mysql_stmt_errno(req->prep), mysql_stmt_error(req->prep));
1983 ret++;
1984 }
1985
1986end:
1987 va_end(ap);
1988
1989 if (-1 == clock_gettime(CLOCK_REALTIME, &dbLast))
1990 perror_msg("Unable to get the time.");
1991 n = (dbLast.tv_sec * 1000000000.0) + dbLast.tv_nsec;
1992 T("dbDoSomething(%s) took %lf seconds", req->sql, (n - t) / 1000000000.0);
1993
1994 return ret;
1995}
1996
1997// Copy the SQL results into the request structure.
1998void dbPull(reqData *Rd, char *table, rowData *rows)
1999{
2000 char *where;
2001 qhashtbl_t *me = rows->rows->popfirst(rows->rows, NULL);
2002 qhashtbl_obj_t obj;
2003
2004 if (NULL != me)
2005 {
2006 memset((void*)&obj, 0, sizeof(obj));
2007 me->lock(me);
2008 while(me->getnext(me, &obj, false) == true)
2009 {
2010 where = xmprintf("%s.%s", table, obj.name);
2011d("dbPull(Rd->database) %s = %s", where, (char *) obj.data);
2012 Rd->database->putstr(Rd->database, where, (char *) obj.data);
2013 free(where);
2014 }
2015 me->unlock(me);
2016 me->free(me);
2017 }
2018 free(rows->fieldNames);
2019 rows->rows->free(rows->rows);
2020 free(rows);
2021}
2022
2023my_ulonglong dbCount(char *table, char *where)
2024{
2025 my_ulonglong ret = 0;
2026 char *sql;
2027 struct timespec now, then;
2028
2029 if (-1 == clock_gettime(CLOCK_REALTIME, &then))
2030 perror_msg("Unable to get the time.");
2031
2032 if (where)
2033 sql = xmprintf("SELECT Count(*) FROM %s WHERE %s", table, where);
2034 else
2035 sql = xmprintf("SELECT Count(*) FROM %s", table);
2036
2037 if (mysql_query(database, sql))
2038 {
2039// E("MariaDB error %d - Query failed 1: %s", mysql_errno(database), mysql_error(database));
2040 if (dbCheckError("Query failed 1", sql))
2041 {
2042 ret = dbCount(table, where);
2043 free(sql);
2044 return ret;
2045 }
2046 }
2047 else
2048 {
2049 MYSQL_RES *result = mysql_store_result(database);
2050
2051 if (!result)
2052 E("Couldn't get results set from %s\n: %s", sql, mysql_error(database));
2053 else
2054 {
2055 MYSQL_ROW row = mysql_fetch_row(result);
2056 if (!row)
2057 E("MariaDB error %d - Couldn't get row from %s\n: %s", mysql_errno(database), sql, mysql_error(database));
2058 else
2059 ret = atoll(row[0]);
2060 mysql_free_result(result);
2061 }
2062 }
2063
2064 if (-1 == clock_gettime(CLOCK_REALTIME, &now))
2065 perror_msg("Unable to get the time.");
2066 double n = (now.tv_sec * 1000000000.0) + now.tv_nsec;
2067 double t = (then.tv_sec * 1000000000.0) + then.tv_nsec;
2068// T("dbCount(%s) took %lf seconds", sql, (n - t) / 1000000000.0);
2069 free(sql);
2070 return ret;
2071}
2072
2073my_ulonglong dbCountJoin(char *table, char *select, char *join, char *where)
2074{
2075 my_ulonglong ret = 0;
2076 char *sql;
2077 struct timespec now, then;
2078
2079 if (-1 == clock_gettime(CLOCK_REALTIME, &then))
2080 perror_msg("Unable to get the time.");
2081
2082 if (NULL == select)
2083 select = "*";
2084 if (NULL == join)
2085 join = "";
2086
2087 if (where)
2088 sql = xmprintf("SELECT %s FROM %s %s WHERE %s", select, table, join, where);
2089 else
2090 sql = xmprintf("SELECT %s FROM %s", select, table, join);
2091
2092 if (mysql_query(database, sql))
2093 {
2094// E("MariaDB error %d - Query failed 2: %s", mysql_errno(database), mysql_error(database));
2095 if (dbCheckError("Query failed 2", sql))
2096 {
2097 ret = dbCountJoin(table, select, join, where);
2098 free(sql);
2099 return ret;
2100 }
2101 }
2102 else
2103 {
2104 MYSQL_RES *result = mysql_store_result(database);
2105
2106 if (!result)
2107 E("MariaDB error %d - Couldn't get results set from %s\n: %s", mysql_errno(database), sql, mysql_error(database));
2108 else
2109 ret = mysql_num_rows(result);
2110 mysql_free_result(result);
2111 }
2112
2113 if (-1 == clock_gettime(CLOCK_REALTIME, &now))
2114 perror_msg("Unable to get the time.");
2115 double n = (now.tv_sec * 1000000000.0) + now.tv_nsec;
2116 double t = (then.tv_sec * 1000000000.0) + then.tv_nsec;
2117// T("dbCointJoin(%s) took %lf seconds", sql, (n - t) / 1000000000.0);
2118 free(sql);
2119 return ret;
2120}
2121
2122
2123void replaceStr(qhashtbl_t *ssi, char *key, char *value)
2124{
2125 ssi->putstr(ssi, key, value);
2126}
2127
2128void replaceLong(qhashtbl_t *ssi, char *key, my_ulonglong value)
2129{
2130 char *tmp = xmprintf("%lu", value);
2131
2132 replaceStr(ssi, key, tmp);
2133 free(tmp);
2134}
2135
2136
2137float timeDiff(struct timeval *now, struct timeval *then)
2138{
2139 if (0 == gettimeofday(now, NULL))
2140 {
2141 struct timeval thisTime = { 0, 0 };
2142 double result = 0.0;
2143
2144 thisTime.tv_sec = now->tv_sec;
2145 thisTime.tv_usec = now->tv_usec;
2146 if (thisTime.tv_usec < then->tv_usec)
2147 {
2148 thisTime.tv_sec--;
2149 thisTime.tv_usec += 1000000;
2150 }
2151 thisTime.tv_usec -= then->tv_usec;
2152 thisTime.tv_sec -= then->tv_sec;
2153 result = ((double) thisTime.tv_usec) / ((double) 1000000.0);
2154 result += thisTime.tv_sec;
2155 return result;
2156 }
2157
2158 return 0.0;
2159}
2160
2161
2162gridStats *getStats(MYSQL *db, gridStats *stats)
2163{
2164 if (NULL == stats)
2165 {
2166 stats = xmalloc(sizeof(gridStats));
2167 stats->next = 30;
2168 gettimeofday(&(stats->last), NULL);
2169 stats->stats = qhashtbl(0, 0);
2170 stats->stats->putstr(stats->stats, "version", "SledjChisl FCGI Dev 0.1");
2171 stats->stats->putstr(stats->stats, "grid", "my grid");
2172 stats->stats->putstr(stats->stats, "uri", "http://localhost:8002/");
2173 if (checkSimIsRunning("ROBUST"))
2174 stats->stats->putstr(stats->stats, "gridOnline", "online");
2175 else
2176 stats->stats->putstr(stats->stats, "gridOnline", "offline");
2177 }
2178 else
2179 {
2180 static struct timeval thisTime;
2181 if (stats->next > timeDiff(&thisTime, &(stats->last)))
2182 return stats;
2183 }
2184
2185 I("Getting fresh grid stats.");
2186 if (checkSimIsRunning("ROBUST"))
2187 replaceStr(stats->stats, "gridOnline", "online");
2188 else
2189 replaceStr(stats->stats, "gridOnline", "offline");
2190
2191 char *tmp;
2192 my_ulonglong locIn = dbCount("Presence", "RegionID != '00000000-0000-0000-0000-000000000000'"); // Locals online but not HGing, and HGers in world.
2193 my_ulonglong HGin = dbCount("Presence", "UserID NOT IN (SELECT PrincipalID FROM UserAccounts)"); // HGers in world.
2194
2195 // Collect stats about members.
2196 replaceLong(stats->stats, "hgers", HGin);
2197 if (locIn >= HGin) // Does OpenSim have too many ghosts?
2198 replaceLong(stats->stats, "inworld", locIn - HGin);
2199 else
2200 replaceLong(stats->stats, "inworld", 0);
2201 tmp = xmprintf("GridExternalName != '%s'", stats->stats->getstr(stats->stats, "uri", false));
2202 replaceLong(stats->stats, "outworld", dbCount("hg_traveling_data", tmp));
2203 free(tmp);
2204 replaceLong(stats->stats, "members", dbCount("UserAccounts", NULL));
2205
2206 // Count local and HG visitors for the last 30 and 60 days.
2207 locIn = dbCountJoin("GridUser", "GridUser.UserID", "INNER JOIN UserAccounts ON GridUser.UserID = UserAccounts.PrincipalID",
2208 "Login > UNIX_TIMESTAMP(FROM_UNIXTIME(UNIX_TIMESTAMP(now()) - 2419200))");
2209 HGin = dbCount("GridUser", "Login > UNIX_TIMESTAMP(FROM_UNIXTIME(UNIX_TIMESTAMP(now()) - 2419200))");
2210 replaceLong(stats->stats, "locDay30", locIn);
2211 replaceLong(stats->stats, "day30", HGin);
2212 replaceLong(stats->stats, "HGday30", HGin - locIn);
2213
2214 locIn = dbCountJoin("GridUser", "GridUser.UserID", "INNER JOIN UserAccounts ON GridUser.UserID = UserAccounts.PrincipalID",
2215 "Login > UNIX_TIMESTAMP(FROM_UNIXTIME(UNIX_TIMESTAMP(now()) - 4838400))");
2216 HGin = dbCount("GridUser", "Login > UNIX_TIMESTAMP(FROM_UNIXTIME(UNIX_TIMESTAMP(now()) - 4838400))");
2217 replaceLong(stats->stats, "locDay60", locIn);
2218 replaceLong(stats->stats, "day60", HGin);
2219 replaceLong(stats->stats, "HGday60", HGin - locIn);
2220
2221 // Collect stats about sims.
2222 replaceLong(stats->stats, "sims", dbCount("regions", NULL));
2223 replaceLong(stats->stats, "onlineSims", dbCount("regions", "sizeX != 0"));
2224 replaceLong(stats->stats, "varRegions", dbCount("regions", "sizeX > 256 or sizeY > 256"));
2225 replaceLong(stats->stats, "singleSims", dbCount("regions", "sizeX = 256 and sizeY = 256"));
2226 replaceLong(stats->stats, "offlineSims", dbCount("regions", "sizeX = 0"));
2227
2228 // Calculate total size of all regions.
2229 my_ulonglong simSize = 0;
2230 static dbRequest *rgnSizes = NULL;
2231 if (NULL == rgnSizes)
2232 {
2233 static char *szi[] = {NULL};
2234 static char *szo[] = {"sizeX", "sizeY", NULL};
2235 rgnSizes = xzalloc(sizeof(dbRequest));
2236 rgnSizes->table = "regions";
2237 rgnSizes->inParams = szi;
2238 rgnSizes->outParams = szo;
2239 rgnSizes->where = "sizeX != 0";
2240 dbRequests->addfirst(dbRequests, &rgnSizes, sizeof(dbRequest *));
2241 }
2242 dbDoSomething(rgnSizes, FALSE); // LEAKY
2243 rowData *rows = rgnSizes->rows;
2244
2245 qhashtbl_t *row;
2246 while (NULL != (row = rows->rows->getat(rows->rows, 0, NULL, true)))
2247 {
2248 my_ulonglong x = 0, y = 0;
2249
2250 tmp = row->getstr(row, "sizeX", false);
2251 if (NULL == tmp)
2252 E("No regions.sizeX!");
2253 else
2254 x = atoll(tmp);
2255 tmp = row->getstr(row, "sizeY", false);
2256 if (NULL == tmp)
2257 E("No regions.sizeY!");
2258 else
2259 y = atoll(tmp);
2260 simSize += x * y;
2261 row->free(row);
2262 rows->rows->removefirst(rows->rows);
2263 }
2264 free(rows->fieldNames);
2265 rows->rows->free(rows->rows);
2266 free(rows);
2267
2268 tmp = xmprintf("%lu", simSize);
2269 stats->stats->putstr(stats->stats, "simsSize", tmp);
2270 free(tmp);
2271 gettimeofday(&(stats->last), NULL);
2272
2273 return stats;
2274}
2275
2276
2277qhashtbl_t *toknize(char *text, char *delims)
2278{
2279 qhashtbl_t *ret = qhashtbl(0, 0);
2280
2281 if (NULL == text)
2282 return ret;
2283
2284 char *txt = xstrdup(text), *token, dlm = ' ', *key, *val = NULL;
2285 int offset = 0;
2286
2287 while((token = qstrtok(txt, delims, &dlm, &offset)) != NULL)
2288 {
2289 if (delims[0] == dlm)
2290 {
2291 key = token;
2292 val = &txt[offset];
2293 }
2294 else if (delims[1] == dlm)
2295 {
2296 ret->putstr(ret, qstrtrim_head(key), token);
2297d(" %s = %s", qstrtrim_head(key), val);
2298 val = NULL;
2299 }
2300 }
2301 if (NULL != val)
2302{
2303 ret->putstr(ret, qstrtrim_head(key), val);
2304d(" %s = %s", qstrtrim_head(key), val);
2305}
2306 free(txt);
2307 return ret;
2308}
2309
2310void santize(qhashtbl_t *tbl)
2311{
2312 qhashtbl_obj_t obj;
2313
2314 memset((void*)&obj, 0, sizeof(obj));
2315 tbl->lock(tbl);
2316 while(tbl->getnext(tbl, &obj, false) == true)
2317 {
2318 char *n = obj.name, *o = (char *) obj.data;
2319
2320 qurl_decode(o);
2321 tbl->putstr(tbl, n, o);
2322 }
2323 tbl->unlock(tbl);
2324}
2325
2326void outize(qgrow_t *reply, qhashtbl_t *tbl, char *label)
2327{
2328 reply->addstrf(reply, "%s:<br>\n<pre>\n", label);
2329 qhashtbl_obj_t obj;
2330 memset((void*)&obj, 0, sizeof(obj));
2331 tbl->lock(tbl);
2332 while(tbl->getnext(tbl, &obj, false) == true)
2333 reply->addstrf(reply, " &nbsp; %s = %s\n", obj.name, (char *) obj.data);
2334 tbl->unlock(tbl);
2335 reply->addstr(reply, "</pre>\n");
2336}
2337
2338// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie
2339enum cookieSame
2340{
2341 CS_NOT,
2342 CS_STRICT,
2343 CS_LAX, // Apparently the default set by browsers these days.
2344 CS_NONE
2345};
2346typedef struct _cookie cookie;
2347struct _cookie
2348{
2349 char *value, *domain, *path;
2350 // char *expires; // Use maxAge instead, it's far simpler to figure out.
2351 int maxAge;
2352 boolean secure, httpOnly;
2353 enum cookieSame site;
2354};
2355
2356void freeCookie(reqData *Rd, char *cki)
2357{
2358 cookie *ck0 = Rd->Rcookies->get(Rd->Rcookies, cki, NULL, false);
2359
2360 if (NULL != ck0)
2361 {
2362 if (NULL != ck0->value)
2363 free(ck0->value);
2364 Rd->Rcookies->remove(Rd->Rcookies, cki);
2365 }
2366}
2367
2368cookie *setCookie(reqData *Rd, char *cki, char *value)
2369{
2370 cookie *ret = xzalloc(sizeof(cookie));
2371 char *cook = xstrdup(cki);
2372 int l, i;
2373
2374// TODO - would URL encoding do the trick?
2375 // Validate this, as there is a limited set of characters allowed.
2376 qstrreplace("tr", cook, "()<>@,;:\\\"/[]?={} \t", "_");
2377 freeCookie(Rd, cook);
2378 l = strlen(cook);
2379 for (i = 0; i < l; i++)
2380 {
2381 if (iscntrl(cook[i]) != 0)
2382 cook[i] = '_';
2383 }
2384 l = strlen(value);
2385 if (0 != l)
2386 ret->value = qurl_encode(value, l);
2387 else
2388 ret->value = xstrdup("");
2389 ret->httpOnly = TRUE;
2390 ret->site = CS_STRICT;
2391 ret->secure = TRUE;
2392 ret->path = getStrH(Rd->headers, "SCRIPT_NAME");
2393 Rd->Rcookies->put(Rd->Rcookies, cook, ret, sizeof(cookie));
2394 free(ret);
2395 ret = Rd->Rcookies->get(Rd->Rcookies, cook, NULL, false);
2396 free(cook);
2397 return ret;
2398}
2399
2400char *getCookie(qhashtbl_t *cookies, char *cki)
2401{
2402 char *ret = NULL;
2403 cookie *ck = (cookie *) cookies->get(cookies, cki, NULL, false);
2404
2405 if (NULL != ck)
2406 ret = ck->value;
2407 return ret;
2408}
2409
2410void outizeCookie(qgrow_t *reply, qhashtbl_t *tbl, char *label)
2411{
2412 reply->addstrf(reply, "%s:<br>\n<pre>\n", label);
2413 qhashtbl_obj_t obj;
2414 memset((void*)&obj, 0, sizeof(obj));
2415 tbl->lock(tbl);
2416 while(tbl->getnext(tbl, &obj, false) == true)
2417 reply->addstrf(reply, " &nbsp; %s = %s\n", obj.name, ((cookie *) obj.data)->value);
2418 tbl->unlock(tbl);
2419 reply->addstr(reply, "</pre>\n");
2420}
2421
2422void list2cookie(reqData *Rd, char *cki, qlist_t *list)
2423{
2424 char *t0 = xstrdup("");
2425 qlist_obj_t obj;
2426
2427 memset((void*)&obj, 0, sizeof(obj)); // must be cleared before call
2428 list->lock(list);
2429 while (list->getnext(list, &obj, false) == true)
2430 {
2431 char *t1 = xmprintf("%s\n%s", t0, (char *) obj.data);
2432
2433 free(t0);
2434 t0 = t1;
2435 }
2436 list->unlock(list);
2437 setCookie(Rd, cki, &t0[1]); // Skip the first empty one.
2438// TODO - should set a very short maxAge.
2439 free(t0);
2440}
2441
2442qlist_t *cookie2list(qhashtbl_t *cookies, char *cki)
2443{
2444 qlist_t *ret = NULL;
2445 cookie *ck = (cookie *) cookies->get(cookies, cki, NULL, false);
2446
2447 if (NULL != ck)
2448 {
2449 if (NULL != ck->value)
2450 {
2451 qurl_decode(ck->value);
2452 ret = qstrtokenizer(ck->value, "\n");
2453 free(ck->value);
2454 }
2455// TODO - should send the "delete this cookie" thing to the browser.
2456 cookies->remove(cookies, cki);
2457 }
2458 return ret;
2459}
2460
2461
2462enum fragmentType
2463{
2464 FT_TEXT,
2465 FT_PARAM,
2466 FT_LUA
2467};
2468
2469typedef struct _fragment fragment;
2470struct _fragment
2471{
2472 enum fragmentType type;
2473 int length;
2474 char *text;
2475};
2476
2477static void HTMLdebug(qgrow_t *reply)
2478{
2479 reply->addstrf(reply,
2480 " <p class='hoverItem'>\n"
2481 " <div class='hoverWrapper0'>\n"
2482 " <p>DEBUG</p>\n"
2483 " <div id='hoverShow0'>\n"
2484 " <h1>DEBUG log</h1>\n"
2485 " <!--#echo var=\"DEBUG\" -->\n"
2486 " </div>\n"
2487 " </div>\n"
2488 " </p>\n"
2489 );
2490}
2491
2492static void HTMLheader(qgrow_t *reply, char *title)
2493{
2494 reply->addstrf(reply,
2495 "<html>\n"
2496 " <head>\n"
2497 " <title>%s</title>\n"
2498 " <meta charset=\"UTF-8\">\n"
2499 " <link rel=\"shortcut icon\" href=\"/SledjHamrIconSmall.png\">\n"
2500 , title);
2501 reply->addstrf(reply, " <link type='text/css' rel='stylesheet' href='/SledjChisl.css' media='all' />\n");
2502
2503 if (DEBUG)
2504 reply->addstrf(reply, " <link type='text/css' rel='stylesheet' href='/debugStyle.css' media='all' />\n");
2505
2506 reply->addstrf(reply,
2507 " <style> \n"
2508 " html, body {background-color: black; color: white; font-family: 'sans-serif'; margin: 0; padding: 0;}\n"
2509 " a:link {color: aqua;}\n"
2510 " a:visited {color: fuchsia;}\n"
2511 " a:hover {color: blue;}\n"
2512 " a:active {color: red;}\n"
2513 " button {background-color: darkgreen; color: white; font-family: 'sans-serif';}\n"
2514 " button:hover {color: blue;}\n"
2515 " button:active {color: red;}\n"
2516 " label {background-color:darkgreen; color: white; font-family: 'sans-serif'; font-size: 160%;}\n"
2517 " input {background-color:darkblue; color: white; font-family: 'sans-serif'; font-size: 80%;}\n"
2518 // What idiot thought aligning the label with the bottom of textareas was a good default?
2519 " textarea {background-color:darkblue; color: white; font-family: 'sans-serif'; font-size: 80%; vertical-align: top;}\n"
2520 " </style>\n"
2521 " </head>\n"
2522 " <body bgcolor='black' text='white' link='aqua' vlink='fuchsia' alink='red'>\n"
2523 " <font face='sans-serif'>\n"
2524 );
2525 reply->addstrf(reply, " <div class='top-left'>\n");
2526 if (DEBUG)
2527 HTMLdebug(reply);
2528}
2529
2530// TODO - maybe escape non printables as well?
2531char *HTMLentities[] =
2532{
2533 "", "", "", "", "", "", "", "", "", // NUL SOH STX ETX EOT ENQ ACK BEL BS
2534 "&Tab;", "&NewLine;",
2535 "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", // VT FF CR SO SI DLE DC1 DC2 DC3 DC4 NAK SYN ETB CAN EM SUB ESC FS GS RS US
2536 " ", // Space
2537 "&excl;", "&quot;",
2538 "&num;", "&dollar;",
2539 "&percnt;", "&amp;",
2540 "&apos;",
2541 "&lpar;", "&rpar;",
2542 "&ast;",
2543 "&plus;",
2544 "&comma;",
2545 "-",
2546 "&period;",
2547 "&sol;",
2548 "0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
2549 "&colon;", "&semi;",
2550 "&lt;", "&equals;", "&gt;",
2551 "&quest;",
2552 "&commat;",
2553 "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z",
2554 "&lsqb;", "&bsol;", "&rsqb;",
2555 "&Hat;",
2556 "&lowbar;",
2557 "&grave;",
2558 "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z",
2559 "&lcub;", "&verbar;", "&rcub;",
2560 "~",
2561 "", // DEL
2562 "&nbsp;" // This is actually 160, not 128, but I hack around that.
2563};
2564static void HTMLescapeString(qgrow_t *reply, char *string)
2565{
2566 size_t l = strlen(string);
2567 char *t = xmalloc(l * 10 + 1);
2568 int i, j = 0;
2569 boolean space = FALSE;
2570
2571 for (i = 0; i < l; i++)
2572 {
2573 int s = string[i];
2574
2575 // Alternate long line of spaces with space and &nbsp;.
2576 if (' ' == s)
2577 {
2578 if (space)
2579 {
2580 s = 128;
2581 space = FALSE;
2582 }
2583 else
2584 space = TRUE;
2585 }
2586 else
2587 {
2588 space = FALSE;
2589 if (128 == s) // The real 128 character.
2590 {
2591 t[j++] = ' ';
2592 continue;
2593 }
2594 }
2595
2596 if (128 >= s)
2597 {
2598 char *r = HTMLentities[s];
2599 size_t m = strlen(r);
2600 int k;
2601
2602 for (k = 0; k < m; k++)
2603 t[j++] = r[k];
2604 }
2605 else
2606 t[j++] = ' ';
2607 }
2608 t[j] = '\0';
2609 reply->addstr(reply, t);
2610 free(t);
2611}
2612
2613static void HTMLtable(qgrow_t *reply, MYSQL *db, MYSQL_RES *result, char *caption, char *URL, char *id)
2614{
2615 char *tbl = "";
2616 char *address, *addrend, *t, *t0;
2617 int count = 0, c = -1, i;
2618 MYSQL_ROW row;
2619 MYSQL_FIELD *fields = mysql_fetch_fields(result);
2620
2621 reply->addstrf(reply, "<table border=\"1\"><caption>%s</caption>\n", caption);
2622
2623 if (!fields)
2624 E("Failed fetching fields: %s", mysql_error(db));
2625 while ((row = mysql_fetch_row(result)))
2626 {
2627 reply->addstr(reply, "<tr>");
2628 address = xmprintf("");
2629 addrend = "";
2630
2631 if (-1 == c)
2632 c = mysql_num_fields(result);
2633
2634 if (0 == count)
2635 {
2636 for (i = 0; i < c; i++)
2637 {
2638 char *s = fields[i].name;
2639
2640 reply->addstrf(reply, "<th>%s</th>", s);
2641 }
2642 reply->addstr(reply, "</tr>\n<tr>");
2643 }
2644
2645 if (NULL != URL)
2646 {
2647 free(address);
2648 address = xmprintf("<a href=\"%s", URL);
2649 addrend = "</a>";
2650 }
2651
2652 for (i = 0; i < c; i++)
2653 {
2654 char *s = fields[i].name;
2655
2656 t0 = row[i];
2657 if (NULL == t0)
2658 E("No field %s!", s);
2659 else
2660 {
2661 if ((NULL != id) && (strcmp(s, id) == 0))
2662 reply->addstrf(reply, "<td>%s&%s=%s\">%s%s</td>", address, id, t0, t0, addrend);
2663 else
2664 reply->addstrf(reply, "<td>%s</td>", t0);
2665 }
2666 }
2667 reply->addstr(reply, "</tr>\n");
2668
2669 free(address);
2670 count++;
2671 }
2672
2673 reply->addstr(reply, "</table>");
2674 mysql_free_result(result);
2675}
2676
2677static void HTMLhidden(qgrow_t *reply, char *name, char *val)
2678{
2679 if ((NULL != val) && ("" != val))
2680 {
2681 reply->addstrf(reply, " <input type=\"hidden\" name=\"%s\" value=\"", name);
2682 HTMLescapeString(reply, val);
2683 reply->addstr(reply, "\">\n");
2684 }
2685}
2686
2687static void HTMLform(qgrow_t *reply, char *action, char *token)
2688{
2689 reply->addstrf(reply, " <form action=\"%s\" method=\"POST\">\n", action);
2690 if ((NULL != token) && ('\0' != token[0]))
2691 HTMLhidden(reply, "munchie", token);
2692}
2693static void HTMLformEnd(qgrow_t *reply)
2694{
2695 reply->addstrf(reply, " </form>\n");
2696}
2697
2698static void HTMLcheckBox(qgrow_t *reply, char *name, char *title, boolean checked, boolean required)
2699{
2700 // HTML is an absolute fucking horror. This is so that we got an off sent to us if the checkbox is off, otherwise we get nothing.
2701 HTMLhidden(reply, name, "off");
2702 reply->addstrf(reply, " <p>");
2703 reply->addstrf(reply, "<label><input type=\"checkbox\" name=\"%s\"", name);
2704 if (checked)
2705 reply->addstrf(reply, " checked", name);
2706// if (required)
2707// reply->addstr(reply, " required");
2708 reply->addstrf(reply, "> %s </label>", title);
2709// reply->addstrf(reply, "> %s ⛝ □ 🞐 🞎 🞎 ☐ ▣️ ◉ ○ </label>", title);
2710 reply->addstrf(reply, "</p>\n");
2711}
2712
2713static void HTMLtextArea(qgrow_t *reply, char *name, char *title, int rows, int cols, int min, int max, char *holder, char *comp, char *spell, char *wrap, char *val, boolean required, boolean readOnly)
2714{
2715 reply->addstrf(reply, " <p><label>%s : <textarea name=\"%s\"", title, name);
2716 if (0 < rows)
2717 reply->addstrf(reply, " rows=\"%d\"", rows);
2718 if (0 < cols)
2719 reply->addstrf(reply, " cols=\"%d\"", cols);
2720 if (0 < min)
2721 reply->addstrf(reply, " minlength=\"%d\"", min);
2722 if (0 < max)
2723 reply->addstrf(reply, " maxlength=\"%d\"", max);
2724 if (required)
2725 reply->addstr(reply, " required");
2726 if (readOnly)
2727 reply->addstr(reply, " readonly");
2728 if ("" != holder)
2729 reply->addstrf(reply, " placeholder=\"%s\"", holder);
2730 if ("" != comp)
2731 reply->addstrf(reply, " autocomplete=\"%s\"", comp);
2732 if ("" != spell)
2733 reply->addstrf(reply, " spellcheck=\"%s\"", spell);
2734 if ("" != wrap)
2735 reply->addstrf(reply, " wrap=\"%s\"", wrap);
2736 if ((NULL != val) && ("" != val))
2737 {
2738 reply->addstr(reply, ">");
2739 HTMLescapeString(reply, val);
2740 reply->addstr(reply, "</textarea></label></p>\n");
2741 }
2742 else
2743 reply->addstrf(reply, "></textarea></label></p>\n");
2744}
2745
2746static void HTMLtext(qgrow_t *reply, char *type, char *title, char *name, char *val, int size, int max, boolean required)
2747{
2748 reply->addstrf(reply, " <p><label>%s : <input type=\"%s\" name=\"%s\"", title, type, name);
2749 if ((NULL != val) && ("" != val))
2750 {
2751 reply->addstr(reply, " value=\"");
2752 HTMLescapeString(reply, val);
2753 reply->addstr(reply, "\"");
2754 }
2755 if (0 < size)
2756 reply->addstrf(reply, " size=\"%d\"", size);
2757 if (0 < max)
2758 reply->addstrf(reply, " maxlength=\"%d\"", max);
2759 if (required)
2760 reply->addstr(reply, " required");
2761 reply->addstr(reply, "></label></p>\n");
2762}
2763
2764static void HTMLselect(qgrow_t *reply, char *title, char *name)
2765{
2766 if (NULL == title)
2767 reply->addstrf(reply, " <p><select name=\"%s\">", name);
2768 else
2769 reply->addstrf(reply, " <p><label>%s : \n <select name=\"%s\">\n", title, name);
2770}
2771static void HTMLselectEnd(qgrow_t *reply)
2772{
2773 reply->addstr(reply, " </select></label></p>\n \n");
2774}
2775static void HTMLselectEndNo(qgrow_t *reply)
2776{
2777 reply->addstr(reply, " </select></p>");
2778}
2779
2780static void HTMLoption(qgrow_t *reply, char *title, boolean selected)
2781{
2782 char *sel = "";
2783
2784 if (selected)
2785 sel = " selected";
2786 reply->addstrf(reply, " <option value=\"%s\"%s>%s</option>\n", title, sel, title);
2787}
2788
2789static void HTMLbutton(qgrow_t *reply, char *name, char *title)
2790{
2791 reply->addstrf(reply, " <button type=\"submit\" name=\"doit\" value=\"%s\">%s</button>\n", name, title);
2792}
2793
2794static void HTMLlist(qgrow_t *reply, char *title, qlist_t *list)
2795{
2796 qlist_obj_t obj;
2797
2798 reply->addstrf(reply, "<ul>%s\n", title);
2799 memset((void*)&obj, 0, sizeof(obj)); // must be cleared before call
2800 list->lock(list);
2801 while (list->getnext(list, &obj, false) == true)
2802 {
2803 reply->addstr(reply, "<li>");
2804 HTMLescapeString(reply, (char *) obj.data);
2805 reply->addstr(reply, "</li>\n");
2806 }
2807 list->unlock(list);
2808 reply->addstr(reply, "</ul>\n");
2809}
2810
2811static int count = 0;
2812void HTMLfill(reqData *Rd, enum fragmentType type, char *text, int length)
2813{
2814 char *tmp;
2815
2816 switch (type)
2817 {
2818 case FT_TEXT:
2819 {
2820 if (length)
2821 Rd->reply->add(Rd->reply, (void *) text, length * sizeof(char));
2822 break;
2823 }
2824
2825 case FT_PARAM:
2826 {
2827 if (strcmp("DEBUG", text) == 0)
2828 {
2829 if (DEBUG)
2830 {
2831 Rd->reply->addstrf(Rd->reply, "<h1>FastCGI SledjChisl</h1>\n"
2832 "<p>Request number %d, Process ID: %d</p>\n", count++, getpid());
2833 Rd->reply->addstrf(Rd->reply, "<p>libfcgi version: %s</p>\n", FCGI_VERSION);
2834 Rd->reply->addstrf(Rd->reply, "<p>Lua version: %s</p>\n", LUA_RELEASE);
2835 Rd->reply->addstrf(Rd->reply, "<p>LuaJIT version: %s</p>\n", LUAJIT_VERSION);
2836 Rd->reply->addstrf(Rd->reply, "<p>MySQL client version: %s</p>\n", mysql_get_client_info());
2837 outize(Rd->reply, Rd->headers, "Environment");
2838 outize(Rd->reply, Rd->cookies, "Cookies");
2839 outize(Rd->reply, Rd->queries, "Query");
2840 outize(Rd->reply, Rd->body, "POST body");
2841 outize(Rd->reply, Rd->stuff, "Stuff");
2842 showSesh(Rd->reply, &Rd->shs);
2843 if (Rd->lnk) showSesh(Rd->reply, Rd->lnk);
2844 outize(Rd->reply, Rd->database, "Database");
2845 outizeCookie(Rd->reply, Rd->Rcookies, "Reply Cookies");
2846 outize(Rd->reply, Rd->Rheaders, "Reply HEADERS");
2847 }
2848 }
2849 else if (strcmp("URL", text) == 0)
2850 Rd->reply->addstrf(Rd->reply, "%s://%s%s", Rd->Scheme, Rd->Host, Rd->Script);
2851 else
2852 {
2853 if ((tmp = Rd->stats->stats->getstr(Rd->stats->stats, text, false)) != NULL)
2854 Rd->reply->addstr(Rd->reply, tmp);
2855 else
2856 Rd->reply->addstrf(Rd->reply, "<b>%s</b>", text);
2857 }
2858 break;
2859 }
2860
2861 case FT_LUA:
2862 break;
2863 }
2864}
2865
2866static void HTMLfooter(qgrow_t *reply)
2867{
2868 reply->addstrf(reply, " </div>\n");
2869 reply->addstr(reply,
2870 " <div class='top-right'>\n"
2871 " <h1>Experimental account manager</h1>\n"
2872 " <p>This account manager system is currently experimental, and under heavy development. &nbsp; "
2873 " Which means it's not all written yet, and things may break.</p>\n"
2874 " <p>To create an account, choose a name and password, type them in, click the 'create account' button.</p>"
2875 " <p>On the next page, fill in all your details, then click on the 'confirm' button.</p>"
2876 " <p>We follow the usual web site registration process, which sends a validation email, with a link to click. &nbsp; "
2877 " When you get that email, click on the link, or copy it into a web browser.</p>"
2878 " <p>You will then be logged off. &nbsp; Now you have to wait for an admin to approve your new account. &nbsp; "
2879 " They should check with the person you listed as vouching for you first. &nbsp; They will tell you after they approve your account.</p>"
2880 " <p>Missing bits that are still being written - editing accounts, listing accounts, deleting accounts.</p>\n"
2881 " </div>\n");
2882// reply->addstr(reply, " <div class='centre'>\n </div>\n");
2883 reply->addstr(reply,
2884// " <div class='bottom-left'>\n"
2885// " </div>\n"
2886 " <div class='bottom-right'>\n"
2887 " <iframe src='stats.html' style='border:none;height:100%;width:100%;'></iframe>\n"
2888 " </div>\n"
2889 " </font>\n"
2890 "</body>\n</html>\n");
2891}
2892
2893
2894fragment *newFragment(enum fragmentType type, char *text, int len)
2895{
2896 fragment *frg = xmalloc(sizeof(fragment));
2897
2898 frg->type = type;
2899 frg->length = len;
2900 frg->text = xmalloc(len + 1);
2901 memcpy(frg->text, text, len);
2902 frg->text[len] = '\0';
2903 return frg;
2904}
2905
2906qlist_t *fragize(char *mm, size_t length)
2907{
2908 qlist_t *fragments = qlist(QLIST_THREADSAFE);
2909 fragment *frg0, *frg1;
2910
2911 char *h;
2912 int i, j = 0, k = 0, l, m;
2913
2914 // Scan for server side includes style markings.
2915 for (i = 0; i < length; i++)
2916 {
2917 if (i + 5 < length)
2918 {
2919 if (('<' == mm[i]) && ('!' == mm[i + 1]) && ('-' == mm[i + 2]) && ('-' == mm[i + 3]) && ('#' == mm[i + 4])) // '<!--#'
2920 {
2921 m = i;
2922 i += 5;
2923 if (i < length)
2924 {
2925 if (('e' == mm[i]) && ('c' == mm[i + 1]) && ('h' == mm[i + 2]) && ('o' == mm[i + 3]) && (' ' == mm[i + 4])) // 'echo '
2926 {
2927 i += 5;
2928 if (i + 5 < length)
2929 {
2930 if (('v' == mm[i]) && ('a' == mm[i + 1]) && ('r' == mm[i + 2]) && ('=' == mm[i + 3]) && ('"' == mm[i + 4])) // 'var="'
2931 {
2932 i += 5;
2933 for (j = i; j < length; j++)
2934 {
2935 if ('"' == mm[j]) // '"'
2936 {
2937 frg1 = newFragment(FT_PARAM, &mm[i], j - i);
2938 i = j + 1;
2939 if (i + 4 < length)
2940 {
2941 if ((' ' == mm[i]) && ('-' == mm[i + 1]) && ('-' == mm[i + 2]) && ('>' == mm[i + 3])) // ' -->'
2942 i += 4;
2943 }
2944 frg0 = newFragment(FT_TEXT, &mm[k], m - k);
2945 fragments->addlast(fragments, frg0, sizeof(fragment));
2946 fragments->addlast(fragments, frg1, sizeof(fragment));
2947 free(frg0);
2948 free(frg1);
2949 k = i;
2950 break;
2951 }
2952 }
2953 }
2954 }
2955 }
2956 }
2957 }
2958 }
2959 }
2960 frg0 = newFragment(FT_TEXT, &mm[k], length - k);
2961 fragments->addlast(fragments, frg0, sizeof(*frg0));
2962 free(frg0);
2963
2964 return fragments;
2965}
2966
2967void unfragize(qlist_t *fragments, reqData *Rd, boolean fre)
2968{
2969 qlist_obj_t lobj;
2970 memset((void *) &lobj, 0, sizeof(lobj));
2971 fragments->lock(fragments);
2972 while (fragments->getnext(fragments, &lobj, false) == true)
2973 {
2974 fragment *frg = (fragment *) lobj.data;
2975 if (NULL == frg->text)
2976 {
2977 E("NULL fragment!");
2978 continue;
2979 }
2980 if (NULL != Rd)
2981 HTMLfill(Rd, frg->type, frg->text, frg->length);
2982 if (fre)
2983 free(frg->text);
2984 }
2985 fragments->unlock(fragments);
2986 if (fre)
2987 fragments->free(fragments);
2988}
2989
2990HTMLfile *checkHTMLcache(char *file)
2991{
2992 if (NULL == HTMLfileCache)
2993 HTMLfileCache = qhashtbl(0, 0);
2994
2995 HTMLfile *ret = (HTMLfile *) HTMLfileCache->get(HTMLfileCache, file, NULL, true);
2996 int fd = open(file, O_RDONLY);
2997 size_t length = 0;
2998
2999 if (-1 == fd)
3000 {
3001 HTMLfileCache->remove(HTMLfileCache, file);
3002 free(ret);
3003 ret = NULL;
3004 }
3005 else
3006 {
3007 struct stat sb;
3008 if (fstat(fd, &sb) == -1)
3009 {
3010 HTMLfileCache->remove(HTMLfileCache, file);
3011 free(ret);
3012 ret = NULL;
3013 E("Failed to stat %s", file);
3014 }
3015 else
3016 {
3017 if ((NULL != ret) && (ret->last.tv_sec < sb.st_mtim.tv_sec))
3018 {
3019 HTMLfileCache->remove(HTMLfileCache, file);
3020 free(ret);
3021 ret = NULL;
3022 }
3023
3024 if (NULL == ret)
3025 {
3026 char *mm = MAP_FAILED;
3027
3028 ret = xmalloc(sizeof(HTMLfile));
3029 length = sb.st_size;
3030 ret->last.tv_sec = sb.st_mtim.tv_sec;
3031 ret->last.tv_nsec = sb.st_mtim.tv_nsec;
3032
3033 D("Loading web template %s, which is %d bytes long.", file, length);
3034 mm = mmap(NULL, length, PROT_READ, MAP_SHARED | MAP_POPULATE, fd, 0);
3035 if (mm == MAP_FAILED)
3036 {
3037 HTMLfileCache->remove(HTMLfileCache, file);
3038 free(ret);
3039 ret = NULL;
3040 E("Failed to mmap %s", file);
3041 }
3042 else
3043 {
3044 ret->fragments = fragize(mm, length);
3045 if (-1 == munmap(mm, length))
3046 FCGI_fprintf(FCGI_stderr, "Failed to munmap %s\n", file);
3047
3048 HTMLfileCache->put(HTMLfileCache, file, ret, sizeof(*ret));
3049 }
3050 }
3051 close(fd);
3052 }
3053 }
3054
3055 return ret;
3056}
3057
3058
3059
3060
3061
3062/* TODO -
3063
3064 On new user / password reset.
3065. Both should have all the same security concerns as the login page, they are basically logins.
3066. Codes should be "very long", "(for example, 16 case-sensitive alphanumeric characters)"
3067. "confirm" button hit on "accountCreationPage" or "resetPasswordPage"
3068. generate a new token, keep it around for idleTimeOut (or at least 24 hours), call it .linky instead of .lua
3069. hash the linky for the file name, for the same reason we hash the hashish with pepper for the leaf-node.
3070. Include user level field, new users get -200.
3071. Store the linky itself around somewhere we can find it quickly for logged in users.
3072. store it in the regenerated session
3073. Scratch that, we should never store the raw linky, see above about hashing the linky.
3074
3075. The linky is just like the session token, create it in exactly the same way.
3076. Linky is base64() of the binary, so it's short enough to be a file name, and not too long for the URL.
3077. But we only get to send one of them as a linky URL, no backup cookies / body / headers.
3078. Sooo, need to separate the session stuff out of Rd->stuff.
3079. Use two separate qhashtbl's, Rd->session and Rd->linky.
3080
3081 For new user
3082. create their /opt/opensim_SC/var/lib/users/UUID.lua account record, and symlink firstName_lastName.lua to it.
3083. They can log on,
3084 but all they can do is edit their email to send a new validation code, and enter the validation code.
3085 They can reset their password.
3086. Warn them on login and any page refresh that there is an outstanding validation awaiting them.
3087 For reset password
3088. Let them do things as normal, in case this was just someone being mean to them, coz their email addy might be public.
3089. Including the usual logging out and in again with their old password.
3090. Warn them on login and any page refresh that there is an outstanding password reset awaiting them.
3091. email linky, which is some or all of the token result bits strung together, BASE64 encode the result.
3092. regenerate the usual token
3093. user clicks on the linky (or just enters the linky in a field)
3094. validate the linky token.
3095 compare the level field to the linky type in the linky URL, new users -200 would be "../validateUser/.." and not "../resetPassword/.."
3096. delete the linky token
3097. Particularly important for the forgotten password email, since now that token is in the wild, and is used to reset a password.
3098 Which begs the question, other than being able to recieve the email, how do we tell it's them?
3099 Security questions suck, too easily guessed.
3100 Ask their DoB. Still sucky, coz "hey it's my birthday today" is way too leaky.
3101 This is what Multi Factor Autentication is good for, and that's on the TODO list.
3102 Also, admins should have a harder time doing password resets.
3103 Must be approved by another admin?
3104 Must log onto the server via other means to twiddle something there?
3105 For password reset page
3106 Ask their DoB to help confirm it's them.
3107 validate the DoB, delete tokens and back to the login page if they get it wrong
3108 Should warn people on the accountCreationPage that DoB might be used this way.
3109 ask them for the new password, twice
3110 Create a new passwordSalt and passwordHash, store them in the auth table.
3111. For validate new user page
3112. tell them they have validated
3113. create their OpenSim account UserAccounts.UserTitle and auth tables, not GridUser table
3114. create their GridUser record.
3115. update their UserAccounts.Userlevel and UserAccounts.UserTitle
3116. send them to the login page.
3117. regenerate the usual token
3118? let user stay logged on?
3119 Check best practices for this.
3120
3121 Check password strength.
3122 https://stackoverflow.com/questions/549/the-definitive-guide-to-form-based-website-authentication?rq=1
3123 Has some pointers to resources in the top answers "PART V: Checking Password Strength" section.
3124
3125 "PART VI: Much More - Or: Preventing Rapid-Fire Login Attempts" and "PART VII: Distributed Brute Force Attacks" is also good for -
3126 Login attempt throttling.
3127 Deal with dictionary attacks by slowing down access on password failures etc.
3128
3129 Deal with editing yourself.
3130 Deal with editing others, but only as god.
3131
3132
3133 Regularly delete old session files and ancient newbies.
3134
3135
3136 Salts should be "lengthy" (128 bytes suggested in 2007) and random. Should be per user to. Or use a per user and a global one, and store the global one very securely.
3137 And store hash(salt+password).
3138 On the other hand, the OpenSim / SL password hashing function may be set in concrete in all the viewers. I'll have to find out.
3139 So far I have discovered -
3140 On login server side if the password doesn't start with $1$, then password = "$1$" + Util.Md5Hash(passwd);
3141 remove the "$1$ bit
3142 string hashed = Util.Md5Hash(password + ":" + data.Data["passwordSalt"].ToString());
3143 if (data.Data["passwordHash"].ToString() == hashed)
3144 passwordHash is char(32), and as implied above, doesn't include the $1$ bit. passwordSalt is also char(32)
3145 Cool VL and Impy at least sends the $1$ md5 hashed version over the wire.
3146 Modern viewers obfuscate the details buried deep in C++ OOP crap.
3147 Sent via XMLRPC
3148 MD5 is considered broken since 2013, possibly longer.
3149 Otherwise use a slow hashing function. bcrypt? scrypt? Argon2? PBKDF2?
3150 https://security.stackexchange.com/questions/211/how-to-securely-hash-passwords
3151 Should include a code in the result that tells us which algorithm was used, so we can change the algorithm at a later date. /etc/passwd does this.
3152 Which is what the $1$ bit currently used between server and client is sorta for.
3153
3154+ Would be good to have one more level of this Rd->database stuff, so we know the type of the thing.
3155 While qhashtbl has API for putting strings, ints, and memory, it has none for finding out what type a stored thing is.
3156 Once I have a structure, I add things like "level needed to edit it", "OpenSim db structure to Lua file mapping", and other fanciness.
3157 Would also help with the timestamp being stored for the session, it prints out binary gunk in the DEBUG <div>.
3158 Starting to get into object oriented territory here. B-)
3159 I'll have to do it eventually anyway.
3160 object->tostring(object), and replace the big switch() statements in the existing db code with small functions.
3161 That's why the qLibc stuff has that format, coz C doesn't understand the concept of passing "this" as the first argument.
3162 https://stackoverflow.com/questions/351733/how-would-one-write-object-oriented-code-in-c
3163 https://stackoverflow.com/questions/415452/object-orientation-in-c
3164 http://ooc-coding.sourceforge.net/
3165
3166
3167https://owasp.org/www-project-cheat-sheets/cheatsheets/Input_Validation_Cheat_Sheet.html#Email_Address_Validation
3168https://cheatsheetseries.owasp.org/
3169https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html
3170https://owasp.org/www-project-cheat-sheets/cheatsheets/Authentication_Cheat_Sheet.html
3171https://softwareengineering.stackexchange.com/questions/46716/what-technical-details-should-a-programmer-of-a-web-application-consider-before
3172https://wiki.owasp.org/index.php/OWASP_Guide_Project
3173https://stackoverflow.com/questions/549/the-definitive-guide-to-form-based-website-authentication?rq=1
3174https://owasp.org/www-community/xss-filter-evasion-cheatsheet
3175 A list of example XSS things to try.
3176*/
3177
3178
3179
3180/* Four choices for the token - (https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html)
3181 https://en.wikipedia.org/wiki/Cross-site_request_forgery
3182 Has some more info.
3183
3184Large random value generated by a secure method (getrandom(2)).
3185 Keep it secret, put it in hidden fields, or custom HTTP header (requires JavaScript but more secure than hidden fields).
3186 NOT cookies or GET. Don't log it.
3187Cryptographically sign a session ID and timestamp.
3188 Timestamp is for session timeouts.
3189 Keep it secret, put it in hidden fields, or custom HTTP header (requires JavaScript but more secure than hidden fields).
3190 Needs a secret key server side.
3191A strong HMAC (SHA-256 or better) of a session ID and timestamp.
3192 The above document seems to imply that a key is used for this, the openssl EVP functions don't mention any way of supplying this key.
3193 https://en.wikipedia.org/wiki/HMAC says there is a key as well.
3194 https://www.openssl.org/docs/man1.1.0/man3/HMAC.html HAH! They can have keys. OpenSSL's docs suck.
3195 Token = HMAC(sessionID+timestamp)+timestamp (Yes, timestamp is used twice).
3196 Keep it secret, put it in hidden fields, or custom HTTP header (requires JavaScript but more secure than hidden fields).
3197 Needs a secret key server side.
3198Double cookie
3199 Large random value generated by a secure method set as a cookie and hidden field. Check they match.
3200 Optional - encrypt / salted hash it in another cookie / hidden field.
3201+ Also a resin (BASE64 session key in the query string).
3202 Not such a good idea to have something in the query, coz that screws with bookmarks.
3203 https://security.stackexchange.com/questions/59470/double-submit-cookies-vulnerabilities
3204 Though so far all the pages I find saying this don't say flat out say "use headers instead", though they do say "use HSTS".
3205 https://security.stackexchange.com/questions/220797/is-the-double-submit-cookie-pattern-still-effective
3206+ Includes a work around that I might already be doing.
3207TODO - think it through, is it really secure against session hijacking?
3208TODO - document why we redirect POST to GET, coz it's a pain in the arse, and we have to do things twice.
3209
3210SOOOOO - use double cookie + hidden field.
3211 No headers, coz I need JavaScript to do that.
3212 No hidden field when redirecting post POST to GET, coz GET doesn't get those.
3213 pepper = long pass phrase or some such stored in .sledjChisl.conf.lua, which has to be protected dvs1/opensimsc/0640 as well as the database credentials.
3214 salt = large random value generated by a secure method (getrandom(2)).
3215 seshID = large random value generated by a secure method (getrandom(2)).
3216 timeStamp = mtime of the leaf-node file, set to current time when we are creating the token.
3217 sesh = seshID + timeStamp
3218 munchie = HMAC(sesh) + timeStamp The token hidden field
3219 toke_n_munchie = HMAC(UUID + munchie) The token cookie
3220 hashish = HMACkey(toke_n_munchie, salt) Salted token cookie & linky query
3221? resin = BASE64(hashish) Base64 token cookie
3222 leaf-node = HMACkey(hashish, pepper) Stored token file name
3223
3224 Leaf-node.lua (mtime is timeStamp)
3225 IP, UUID, salt, seshID, user name, passwordSalt, passwordHash (last two for OpenSim password protocol)
3226
3227 The test - (validateSesh() below)
3228 we get hashish and toke_n_munchie
3229 HMACkey(hashish + pepper) -> leaf-node
3230 read leaf-node.lua -> IP, UUID, salt, seshID
3231 get it's mtime -> timeStamp
3232 seshID + timeStamp -> sesh
3233 HMAC(sesh) + timeStamp -> munchie
3234 if we got munchie in the hidden field, compare it
3235 toke_n_munchie == HMAC(UUID + munchie)
3236 For linky it'll be -
3237 HMAC(UUID + munchie) -> toke_n_munchie
3238 hashish == HMACkey(toke_n_munchie + salt)
3239+ If it's too old according to mtime, delete it and logout.
3240
3241I should make it easy to change the HMAC() function. Less important for these short lived sessions, more important for the linky URLs, most important for stored password hashes.
3242 Same for the pepper.
3243
3244The required JavaScript might be like https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#xmlhttprequest--native-javascript-
3245 NOTE - they somehow fucked up that anchor tag.
3246
3247NOTE - storing a pepper on the same RAID array as everything else will be a problem when it comes time to replace one of the disks.
3248 It might have failed badly enough that you can't wipe it, but an attacker can dumpster dive it, replace the broken bit (firmware board), and might get lucky.
3249 Also is a problem with SSD and rust storing good data on bad sectors in the spare sector pool, wear levelling, etc.
3250
3251https://stackoverflow.com/questions/16891729/best-practices-salting-peppering-passwords
3252*/
3253
3254
3255qlisttbl_t *accountLevels = NULL;
3256
3257
3258static void bitch(reqData *Rd, char *message, char *log)
3259{
3260 addStrL(Rd->errors, message);
3261 E("%s %s %s - %s %s", getStrH(Rd->headers, "REMOTE_ADDR"), Rd->shs.UUID, Rd->shs.name, message, log);
3262}
3263
3264/* "A token cookie that references a non-existent session, its value should be replaced immediately to prevent session fixation."
3265https://owasp.org/www-community/attacks/Session_fixation
3266 Which describes the problem, but offers no solution.
3267 See https://stackoverflow.com/questions/549/the-definitive-guide-to-form-based-website-authentication?rq=1.
3268I think this means send a new cookie.
3269 I clear out the cookies and send blank ones with -1 maxAge, so they should get deleted.
3270*/
3271static void bitchSession(reqData *Rd, char *message, char *log)
3272{
3273 if ('\0' != message[0])
3274 addStrL(Rd->errors, message);
3275 C("%s %s %s - %s %s", getStrH(Rd->headers, "REMOTE_ADDR"), Rd->shs.UUID, Rd->shs.name, message, log);
3276 Rd->shs.status = SHS_BOGUS;
3277}
3278
3279
3280// The ancient, insecure since 2011, Second Life / OpenSim password hashing algorithm.
3281char *newSLOSsalt(reqData *Rd)
3282{
3283 char *salt = NULL;
3284 unsigned char *md5hash = xzalloc(17);
3285 char uuid[37];
3286 uuid_t binuuid;
3287
3288 uuid_generate_random(binuuid);
3289 uuid_unparse_lower(binuuid, uuid);
3290 if (!qhashmd5((void *) uuid, strlen(uuid), md5hash))
3291 bitch(Rd, "Internal error.", "newSLOSsalt() - qhashmd5(new uuid) failed.");
3292 else
3293 salt = qhex_encode(md5hash, 16);
3294 free(md5hash);
3295 return salt;
3296}
3297
3298char *checkSLOSpassword(reqData *Rd, char *salt, char *password, char *passwordHash, char *fail)
3299{
3300 char *ret = NULL;
3301 int rt = 0;
3302 unsigned char *md5hash = xzalloc(17);
3303 char *hash = NULL, *passHash = NULL;
3304
3305T("checkSLOSpassword(%s, %s, %s, ", password, salt, passwordHash, fail);
3306 // Calculate passHash.
3307 if (!qhashmd5((void *) password, strlen(password), md5hash))
3308 {
3309 bitch(Rd, "Internal error.", "checkSLOSpassword() - qhashmd5(password) failed.");
3310 rt++;
3311 }
3312 else
3313 {
3314 passHash = qhex_encode(md5hash, 16);
3315 hash = xmprintf("%s:%s", passHash, salt);
3316 if (!qhashmd5((void *) hash, strlen(hash), md5hash))
3317 {
3318 bitch(Rd, "Internal error.", "checkSLOSpassword() - qhashmd5(password:salt) failed.");
3319 rt++;
3320 }
3321 else
3322 {
3323 ret = qhex_encode(md5hash, 16);
3324 }
3325 free(hash);
3326 free(passHash);
3327 }
3328
3329 // If one was passed in, compare it.
3330 if ((NULL != ret) && (NULL != passwordHash) && (strcmp(ret, passwordHash) != 0))
3331 {
3332 bitch(Rd, fail, "Password doesn't match passwordHash");
3333 E(" %s %s - %s != %s", password, salt, ret, passwordHash);
3334 rt++;
3335 free(ret);
3336 ret = NULL;
3337 }
3338 free(md5hash);
3339
3340 return ret;
3341}
3342
3343
3344int LuaToHash(reqData *Rd, char *file, char *var, qhashtbl_t *tnm, int ret, struct stat *st, struct timespec *now, char *type)
3345{
3346 struct timespec then;
3347
3348 if (-1 == clock_gettime(CLOCK_REALTIME, &then))
3349 perror_msg("Unable to get the time.");
3350 I("Reading %s file %s", type, file);
3351 if (0 != stat(file, st))
3352 {
3353 D("No %s file.", file);
3354 perror_msg("Unable to stat %s", file);
3355 ret++;
3356 }
3357 else
3358 {
3359 int status = luaL_loadfile(Rd->L, file), result;
3360
3361 if (status)
3362 {
3363 bitch(Rd, "No such thing.", "Can't load file.");
3364 E("Couldn't load file: %s", lua_tostring(Rd->L, -1));
3365 ret++;
3366 }
3367 else
3368 {
3369 result = lua_pcall(Rd->L, 0, LUA_MULTRET, 0);
3370
3371 if (result)
3372 {
3373 bitch(Rd, "Broken thing.", "Can't run file.");
3374 E("Failed to run script: %s", lua_tostring(Rd->L, -1));
3375 ret++;
3376 }
3377 else
3378 {
3379 lua_getglobal(Rd->L, var);
3380 lua_pushnil(Rd->L);
3381
3382 while(lua_next(Rd->L, -2) != 0)
3383 {
3384 char *n = (char *) lua_tostring(Rd->L, -2);
3385
3386 if (lua_isstring(Rd->L, -1))
3387 {
3388 tnm->putstr(tnm, n, (char *) lua_tostring(Rd->L, -1));
3389d("Lua reading (%s) %s = %s", type, n, getStrH(tnm, n));
3390 }
3391 else
3392 {
3393 char *v = (char *) lua_tostring(Rd->L, -1);
3394 W("Unknown Lua variable type for %s = %s", n, v);
3395 }
3396 lua_pop(Rd->L, 1);
3397 }
3398
3399 if (-1 == clock_gettime(CLOCK_REALTIME, now))
3400 perror_msg("Unable to get the time.");
3401 double n = (now->tv_sec * 1000000000.0) + now->tv_nsec;
3402 double t = (then.tv_sec * 1000000000.0) + then.tv_nsec;
3403 T("Reading %s file took %lf seconds", type, (n - t) / 1000000000.0);
3404 }
3405 }
3406 }
3407
3408 return ret;
3409}
3410
3411
3412char *checkLinky(reqData *Rd)
3413{
3414// TODO - should be from Rd.shs->linky-hashish
3415 char *ret = xstrdup(""), *t0 = getStrH(Rd->stuff, "linky-hashish");
3416
3417 if ('\0' != t0[0])
3418 {
3419 char *t1 = qurl_encode(t0, strlen(t0));
3420 free(ret);
3421 ret = xmprintf("<p><font color='red'><b>You have an email waiting with a validation link in it, please check your email. &nbsp; "
3422 "It will be from %s@%s, and it might be in your spam folder, coz these sorts of emails sometimes end up there. &nbsp; "
3423 "You should add that email address to your contacts, or otherwise let it through your spam filter. &nbsp; "
3424// "<a href='https://%s%s?hashish=%s'>%s</a>"
3425 "</b></font></p>\n",
3426 "grid_no_reply", Rd->Host,
3427 Rd->Host, Rd->RUri
3428// ,t1, t0
3429 );
3430 free(t1);
3431 }
3432 return ret;
3433}
3434
3435
3436static void freeSesh(reqData *Rd, boolean linky, boolean wipe)
3437{
3438 char *file = NULL;
3439 sesh *shs = &Rd->shs;
3440
3441T("free sesh %s %s", linky ? "linky" : "session", wipe ? "wipe" : "delete");
3442 if (linky)
3443 {
3444 shs = Rd->lnk;
3445 file = xmprintf("%s/sessions/%s.linky", scCache, shs->leaf);
3446 }
3447 else
3448 file = xmprintf("%s/sessions/%s.lua", scCache, shs->leaf);
3449
3450 if (wipe)
3451 I("Wiping session %s.", file);
3452 else
3453 I("Deleting session %s.", file);
3454
3455 if ('\0' != shs->leaf[0])
3456 {
3457 if (unlink(file))
3458 perror_msg("Unable to delete %s", file);
3459 }
3460
3461 Rd->body-> remove(Rd->body, "munchie");
3462
3463 freeCookie(Rd, "toke_n_munchie");
3464 freeCookie(Rd, "hashish");
3465 cookie *ck = setCookie(Rd, "toke_n_munchie", "");
3466 cookie *ckh = setCookie(Rd, "hashish", "");
3467 ck->maxAge = -1; // Should expire immediately.
3468 ckh->maxAge = -1; // Should expire immediately.
3469
3470 qhashtbl_obj_t obj;
3471
3472 if (wipe)
3473 {
3474 memset((void*)&obj, 0, sizeof(obj));
3475 Rd->database->lock(Rd->database);
3476 while(Rd->database->getnext(Rd->database, &obj, false) == true)
3477 Rd->database->remove(Rd->database, obj.name);
3478 Rd->database->unlock(Rd->database);
3479 if (NULL != shs->name) free(shs->name);
3480 shs->name = NULL;
3481 if (NULL != shs->UUID) free(shs->UUID);
3482 shs->UUID = NULL;
3483 shs->level = -256;
3484// TODO - should I wipe the rest of Rd->shs as well?
3485 Rd->stuff->remove(Rd->stuff, "name");
3486 Rd->stuff->remove(Rd->stuff, "firstName");
3487 Rd->stuff->remove(Rd->stuff, "lastName");
3488 Rd->stuff->remove(Rd->stuff, "email");
3489 Rd->stuff->remove(Rd->stuff, "passwordSalt");
3490 Rd->stuff->remove(Rd->stuff, "passwordHash");
3491 Rd->stuff->remove(Rd->stuff, "passHash");
3492 Rd->stuff->remove(Rd->stuff, "passSalt");
3493 Rd->stuff->remove(Rd->stuff, "linky-hashish");
3494 }
3495
3496 if (shs->isLinky)
3497 {
3498 free(Rd->lnk);
3499 Rd->lnk = NULL;
3500 }
3501 else
3502 {
3503 shs->leaf[0] = '\0';
3504 }
3505 free(file);
3506}
3507
3508static void setToken_n_munchie(reqData *Rd, boolean linky)
3509{
3510 sesh *shs = &Rd->shs;
3511 char *file;
3512
3513 if (linky)
3514 {
3515 shs = Rd->lnk;
3516 file = xmprintf("%s/sessions/%s.linky", scCache, shs->leaf);
3517 }
3518 else
3519 {
3520 file = xmprintf("%s/sessions/%s.lua", scCache, shs->leaf);
3521 }
3522
3523 struct stat st;
3524 int s = stat(file, &st);
3525
3526 if (!linky)
3527 {
3528 setCookie(Rd, "toke_n_munchie", shs->toke_n_munchie);
3529 setCookie(Rd, "hashish", shs->hashish);
3530 }
3531 char *tnm0 = xmprintf( "toke_n_munchie = \n"
3532 "{\n"
3533 " ['IP']='%s',\n"
3534 " ['salt']='%s',\n"
3535 " ['seshID']='%s',\n",
3536 getStrH(Rd->headers, "REMOTE_ADDR"),
3537 shs->salt,
3538 shs->seshID
3539 );
3540 char *tnm1 = xmprintf(" ['name']='%s',\n ['level']='%d',\n", shs->name, (int) shs->level);
3541 char *tnm2 = xmprintf(" ['UUID']='%s',\n", shs->UUID);
3542 char *tnm3 = xmprintf(" ['passHash']='%s',\n", getStrH(Rd->stuff, "passHash"));
3543 char *tnm4 = xmprintf(" ['passSalt']='%s',\n", getStrH(Rd->stuff, "passSalt"));
3544 char *tnm9 = xmprintf("}\n"
3545 "return toke_n_munchie\n");
3546 int fd = notstdio(xcreate_stdio(file, O_CREAT | O_WRONLY | O_TRUNC | O_CLOEXEC, S_IRUSR | S_IWUSR));
3547 size_t l = strlen(tnm0);
3548
3549 if (s)
3550 I("Creating session %s.", file);
3551 else
3552 C("Updating session %s.", file); // I don't think updates can occur now.
3553t("Write shs %s", tnm0);
3554 if (l != writeall(fd, tnm0, l))
3555 {
3556 perror_msg("Writing %s", file);
3557 freeSesh(Rd, linky, TRUE);
3558 }
3559
3560 if (NULL != shs->name)
3561 {
3562t("Write shs %s", tnm1);
3563 l = strlen(tnm1);
3564 if (l != writeall(fd, tnm1, l))
3565 {
3566 perror_msg("Writing %s", file);
3567 freeSesh(Rd, linky, TRUE);
3568 }
3569 }
3570 if (NULL != shs->UUID)
3571 {
3572t("Write shs %s", tnm2);
3573 l = strlen(tnm2);
3574 if (l != writeall(fd, tnm2, l))
3575 {
3576 perror_msg("Writing %s", file);
3577 freeSesh(Rd, linky, TRUE);
3578 }
3579 }
3580
3581 if ('\0' != getStrH(Rd->stuff, "passHash")[0])
3582 {
3583t("Write shs %s", tnm3);
3584 l = strlen(tnm3);
3585 if (l != writeall(fd, tnm3, l))
3586 {
3587 perror_msg("Writing %s", file);
3588 freeSesh(Rd, linky, TRUE);
3589 }
3590 }
3591
3592 if ('\0' != getStrH(Rd->stuff, "passSalt")[0])
3593 {
3594t("Write shs %s", tnm4);
3595 l = strlen(tnm4);
3596 if (l != writeall(fd, tnm4, l))
3597 {
3598 perror_msg("Writing %s", file);
3599 freeSesh(Rd, linky, TRUE);
3600 }
3601 }
3602
3603 l = strlen(tnm9);
3604 if (l != writeall(fd, tnm9, l))
3605 {
3606 perror_msg("Writing %s", file);
3607 freeSesh(Rd, linky, TRUE);
3608 }
3609 // Set the mtime on the file.
3610 futimens(fd, shs->timeStamp);
3611 xclose(fd);
3612 free(tnm9);
3613 free(tnm4);
3614 free(tnm3);
3615 free(tnm2);
3616 free(tnm1);
3617 free(tnm0);
3618 free(file);
3619
3620 if (linky)
3621 {
3622// TODO - Later use libcurl.
3623 char *first = getStrH(Rd->stuff, "firstName"), *last = getStrH(Rd->stuff, "lastName");
3624// TODO - should be from Rd.shs->linky-hashish
3625 char *t0 = xstrdup(Rd->lnk->hashish), *content, *command;
3626
3627 if ('\0' != t0[0])
3628 {
3629 size_t sz = qhex_decode(t0);
3630 char *t1 = qB64_encode(t0, sz);
3631
3632 content = xmprintf(
3633 "From: grid_no_reply@%s\n"
3634 "Relpy-to: grid_no_reply@%s\n"
3635 "Return-Path: bounce_email@%s\n"
3636 "To: %s\n"
3637 "Subject: Validate your new account on %s\n"
3638 "\n"
3639 "This is an automated validation email sent from %s.\n"
3640 "\n"
3641 "Dear %s %s,\n"
3642 "\n"
3643 "Some one has created the account '%s %s' on \n"
3644 "https://%s%s, and hopefully it was you.\n"
3645 "If it wasn't you, you can ignore this email.\n"
3646 "\n"
3647 "Please go to this web link to validate your new account -\n"
3648 "https://%s%s?hashish=%s\n"
3649 "\n"
3650 "Do not reply to this email.\n"
3651 "\n",
3652 Rd->Host, Rd->Host, Rd->Host,
3653 getStrH(Rd->stuff, "email"),
3654 Rd->Host, Rd->Host,
3655 first, last,
3656 first, last, Rd->Host, Rd->RUri,
3657 Rd->Host, Rd->RUri, t1
3658 );
3659 l = strlen(content);
3660 file = xmprintf("%s/sessions/%s.email", scCache, shs->leaf);
3661 fd = notstdio(xcreate_stdio(file, O_CREAT | O_WRONLY | O_TRUNC | O_CLOEXEC, S_IRUSR | S_IWUSR));
3662
3663 if (l != writeall(fd, content, l))
3664 {
3665 perror_msg("Writing %s", file);
3666// freeSesh(Rd, linky, TRUE);
3667 }
3668 xclose(fd);
3669 I("Sending linky email to %s %s", getStrH(Rd->stuff, "email"), t1);
3670 command = xmprintf("sendmail -oi -t <'%s'", file);
3671 int i = system(command);
3672 if (!WIFEXITED(i))
3673 E("sendmail command failed!");
3674 free(command);
3675 free(file);
3676 free(content);
3677 free(t1);
3678 free(t0);
3679 }
3680 }
3681}
3682
3683
3684static void generateAccountUUID(reqData *Rd)
3685{
3686 // Generate a UUID, check it isn't already being used.
3687 char uuid[37], *where;
3688 uuid_t binuuid;
3689 my_ulonglong users = 0;
3690 int c;
3691
3692 do // UserAccounts.PrincipalID is a unique primary index anyway, but we want the user creation process to be a little on the slow side.
3693 {
3694 struct stat st;
3695
3696 uuid_generate_random(binuuid);
3697 uuid_unparse_lower(binuuid, uuid);
3698 // Try Lua user file.
3699 where = xmprintf("%s/users/%s.lua", scData, uuid);
3700 c = stat(where, &st);
3701 if (c)
3702 users = 1;
3703 free(where);
3704 // Try database.
3705 where = xmprintf("UserAccounts.PrincipalID = '%s'", uuid);
3706 D("Trying new UUID %s.", where);
3707 users = dbCount("UserAccounts", where);
3708 free(where);
3709 } while (users != 0);
3710 if (NULL != Rd->shs.UUID) free(Rd->shs.UUID);
3711 Rd->shs.UUID = xstrdup(uuid);
3712 Rd->shs.level = -200;
3713 Rd->database->putstr(Rd->database, "UserAccounts.PrincipalID", uuid);
3714 Rd->database->putstr(Rd->database, "UserAccounts.Userlevel", "-200");
3715}
3716
3717char *getLevel(short level)
3718{
3719 char *ret = "", *lvl = xmprintf("%d", level);
3720 ret = accountLevels->getstr(accountLevels, lvl, false);
3721 if (NULL == ret)
3722 {
3723 qlisttbl_obj_t obj;
3724
3725 memset((void*)&obj, 0, sizeof(obj)); // must be cleared before call
3726 accountLevels->lock(accountLevels);
3727 while(accountLevels->getnext(accountLevels, &obj, NULL, false) == true)
3728 {
3729 if (atoi(obj.name) <= level)
3730 ret = (char *) obj.data;
3731 }
3732 }
3733 free(lvl);
3734 return ret;
3735}
3736
3737typedef struct _systemFolders systemFolders;
3738struct _systemFolders
3739{
3740 char *name;
3741 short type;
3742};
3743
3744systemFolders sysFolders[] =
3745{
3746 {"My Inventory", 8},
3747 {"Animations", 20},
3748 {"Body Parts", 13},
3749 {"Calling Cards", 2},
3750// {"Friends", 2},
3751// {"All", 2},
3752 {"Clothing", 5},
3753 {"Current Outfit", 46},
3754 {"Favorites", 23},
3755 {"Gestures", 21},
3756 {"Landmarks", 3},
3757 {"Lost And Found", 16},
3758 {"Mesh", 49},
3759 {"My Outfits", 48},
3760 {"My Suitcase", 100}, // All the others are replicated inside.
3761 {"Notecards", 7},
3762 {"Objects", 6},
3763 {"Outfit", 47},
3764 {"Photo Album", 15},
3765 {"Scripts", 10},
3766 {"Sounds", 1},
3767 {"Textures", 0},
3768 {"Trash", 14},
3769 {NULL, -1}
3770};
3771
3772boolean writeLuaDouble(reqData *Rd, int fd, char *file, char *name, double number)
3773{
3774 boolean ret = TRUE;
3775// TODO - putting these in Lua as numbers causes lua_tolstring to barf when we read them. Though Lua is supposed to convert between numbers and strings.
3776 char *t = xmprintf(" ['%s'] = '%f',\n", name, number); // NOTE - default precision is 6 decimal places.
3777 size_t l = strlen(t);
3778
3779 if (l != writeall(fd, t, l))
3780 {
3781 perror_msg("Writing %s", file);
3782 ret = FALSE;
3783 }
3784 free(t);
3785 return ret;
3786}
3787
3788boolean writeLuaInteger(reqData *Rd, int fd, char *file, char *name, long number)
3789{
3790 boolean ret = TRUE;
3791// TODO - putting these in Lua as numbers causes lua_tolstring to barf when we read them. Though Lua is supposed to convert between numbers and strings.
3792 char *t = xmprintf(" ['%s'] = '%ld',\n", name, number);
3793 size_t l = strlen(t);
3794
3795 if (l != writeall(fd, t, l))
3796 {
3797 perror_msg("Writing %s", file);
3798 ret = FALSE;
3799 }
3800 free(t);
3801 return ret;
3802}
3803
3804boolean writeLuaString(reqData *Rd, int fd, char *file, char *name, char *string)
3805{
3806 boolean ret = TRUE;
3807
3808 if (NULL == string)
3809 string = getStrH(Rd->stuff, name);
3810
3811 size_t l = strlen(string);
3812 char *t0 = xmalloc(l * 2 + 1);
3813 int i, j = 0;
3814
3815// TODO - maybe escape other non printables as well?
3816 for (i = 0; i < l; i++)
3817 {
3818 // We don't need to escape [] here, coz we are using '' below. Same applies to ", but do it anyway.
3819 switch(string[i])
3820 {
3821 case '\n':
3822 case '\\':
3823 case '\'':
3824 case '"':
3825 t0[j++] = '\\'; break;
3826 }
3827 if ('\n' == string[i])
3828 t0[j++] = 'n';
3829 else if ('\r' == string[i])
3830 ;
3831 else
3832 t0[j++] = string[i];
3833 }
3834 t0[j] = '\0';
3835
3836 char *t1 = xmprintf(" ['%s'] = '%s',\n", name, t0);
3837
3838 l = strlen(t1);
3839 if (l != writeall(fd, t1, l))
3840 {
3841 perror_msg("Writing %s to %s", name, file);
3842 ret = FALSE;
3843 }
3844 free(t1);
3845 free(t0);
3846 return ret;
3847}
3848
3849static void accountWrite(reqData *Rd)
3850{
3851 char *uuid = getStrH(Rd->database, "UserAccounts.PrincipalID");
3852 char *file = xmprintf("%s/users/%s.lua", scData, uuid);
3853 char *level = getStrH(Rd->database, "UserAccounts.UserLevel");
3854 char *link = (NULL == Rd->lnk) ? "" : Rd->lnk->hashish;
3855 char *tnm = "user = \n{\n";
3856
3857 struct stat st;
3858 int s = stat(file, &st);
3859 int fd = notstdio(xcreate_stdio(file, O_CREAT | O_WRONLY | O_TRUNC | O_CLOEXEC, S_IRUSR | S_IWUSR));
3860 size_t l = strlen(tnm);
3861 uuid_t binuuid;
3862
3863 uuid_clear(binuuid);
3864 if ((NULL != uuid) && ('\0' != uuid[0]))
3865 uuid_parse(uuid, binuuid);
3866 if ((NULL != uuid) && ('\0' != uuid[0]) && (!uuid_is_null(binuuid)))
3867 {
3868 if (s)
3869 I("Creating user %s.", file);
3870 else
3871 I("Updating user %s.", file);
3872
3873 if (l != writeall(fd, tnm, l))
3874 perror_msg("Writing %s", file);
3875 else
3876 {
3877 char *name = Rd->stuff->getstr(Rd->stuff, "name", true);
3878 char *end = "}\nreturn user\n";
3879
3880 if (!writeLuaString (Rd, fd, file, "name", name)) goto notWritten;
3881 if (!writeLuaInteger(Rd, fd, file, "created",
3882 (strcmp("", getStrH(Rd->stuff, "created")) != 0) ? atol(getStrH(Rd->stuff, "created")) : (long) Rd->shs.timeStamp[1].tv_sec)) goto notWritten;
3883 if (!writeLuaString (Rd, fd, file, "email", NULL)) goto notWritten;
3884 if (!writeLuaString (Rd, fd, file, "title", getLevel(atoi(level)))) goto notWritten;
3885 if (!writeLuaString (Rd, fd, file, "level", level)) goto notWritten;
3886 if (!writeLuaInteger(Rd, fd, file, "flags", 0)) goto notWritten;
3887 if (!writeLuaInteger(Rd, fd, file, "active", 1)) goto notWritten;
3888 if (!writeLuaString (Rd, fd, file, "passwordHash", NULL)) goto notWritten;
3889 if (!writeLuaString (Rd, fd, file, "passwordSalt", NULL)) goto notWritten;
3890 if (!writeLuaString (Rd, fd, file, "UUID", uuid)) goto notWritten;
3891 if (!writeLuaString (Rd, fd, file, "DoB", NULL)) goto notWritten;
3892 if (!writeLuaString (Rd, fd, file, "agree", NULL)) goto notWritten;
3893 if (!writeLuaString (Rd, fd, file, "adult", NULL)) goto notWritten;
3894 if (!writeLuaString (Rd, fd, file, "aboutMe", NULL)) goto notWritten;
3895 if (!writeLuaString (Rd, fd, file, "vouched", "off")) goto notWritten;
3896 if (!writeLuaString (Rd, fd, file, "voucher", NULL)) goto notWritten;
3897 if (!writeLuaString (Rd, fd, file, "link", link)) goto notWritten;
3898 l = strlen(end);
3899 if (l != writeall(fd, end, l))
3900 perror_msg("Writing %s", file);
3901 else
3902 {
3903 char *nm = xmprintf("%s/users/%s.lua", scData, qstrreplace("tr", name, " ", "_"));
3904
3905 free(file);
3906 file = xmprintf("%s.lua", uuid);
3907 I("Symlinking %s to %s", file, nm);
3908 if (0 != symlink(file, nm))
3909 perror_msg("Symlinking %s to %s", file, nm);
3910 free(nm);
3911 }
3912notWritten:
3913 free(name);
3914 }
3915 xclose(fd);
3916
3917 short lvl = atoi(level);
3918
3919 if (0 <= lvl) // Note that http://opensimulator.org/wiki/Userlevel claims that 1 and above are "GOD_LIKE".
3920 {
3921 if (Rd->fromDb)
3922 {
3923 I("Updating database user %s.", getStrH(Rd->stuff, "name"));
3924 }
3925 else
3926 {
3927 // Setup the database stuff.
3928 static dbRequest *acntsI = NULL;
3929 if (NULL == acntsI)
3930 {
3931 static char *szi[] =
3932 {
3933 "FirstName",
3934 "LastName",
3935 "Email",
3936 "Created",
3937 "PrincipalID",
3938 "ScopeID",
3939 "UserLevel",
3940 "UserFlags",
3941 "UserTitle",
3942// "ServiceURLs", // No worky "text", filled with crap.
3943 "active",
3944 NULL
3945 };
3946 static char *szo[] = {NULL};
3947 acntsI = xzalloc(sizeof(dbRequest));
3948 acntsI->table = "UserAccounts";
3949 acntsI->inParams = szi;
3950 acntsI->outParams = szo;
3951 acntsI->where = "";
3952 acntsI->type = CT_CREATE;
3953 dbRequests->addfirst(dbRequests, &acntsI, sizeof(dbRequest *));
3954 }
3955 static dbRequest *authI = NULL;
3956 if (NULL == authI)
3957 {
3958 static char *szi[] = {"UUID", "passwordSalt", "passwordHash", "accountType", "webLoginKey", NULL};
3959 static char *szo[] = {NULL};
3960 authI = xzalloc(sizeof(dbRequest));
3961 authI->table = "auth";
3962 authI->inParams = szi;
3963 authI->outParams = szo;
3964 authI->where = "";
3965 authI->type = CT_CREATE;
3966 dbRequests->addfirst(dbRequests, &authI, sizeof(dbRequest *));
3967 }
3968 static dbRequest *invFolderI = NULL;
3969 if (NULL == invFolderI)
3970 {
3971 static char *szi[] =
3972 {
3973 "agentID",
3974 "folderName",
3975 "type", // smallint(6)
3976 "version", // int(11)
3977 "folderID",
3978 "parentFolderID",
3979 NULL
3980 };
3981 static char *szo[] = {NULL};
3982 invFolderI = xzalloc(sizeof(dbRequest));
3983 invFolderI->table = "inventoryfolders";
3984 invFolderI->inParams = szi;
3985 invFolderI->outParams = szo;
3986 invFolderI->where = "";
3987 invFolderI->type = CT_CREATE;
3988 dbRequests->addfirst(dbRequests, &invFolderI, sizeof(dbRequest *));
3989 }
3990 static dbRequest *gUserI = NULL;
3991 if (NULL == gUserI)
3992 {
3993// static char *szi[] = {"UserID", "HomeRegionID", "HomePosition", "HomeLookAt", "LastRegionID", "LastPosition", "LastLookAt", "Online", "Login", "Logout", NULL};
3994 static char *szi[] = {"UserID", NULL}; // All the defaults are what we would set anyway.
3995 static char *szo[] = {NULL};
3996 gUserI = xzalloc(sizeof(dbRequest));
3997 gUserI->table = "GridUser";
3998 gUserI->inParams = szi;
3999 gUserI->outParams = szo;
4000 gUserI->where = "";
4001 gUserI->type = CT_CREATE;
4002 dbRequests->addfirst(dbRequests, &gUserI, sizeof(dbRequest *));
4003 }
4004
4005 I("Creating database user %s %s.", uuid, getStrH(Rd->stuff, "name"));
4006 char *first = Rd->stuff->getstr(Rd->stuff, "name", true), *last = strchr(first, ' ');
4007
4008 // create user record.
4009 *last++ = '\0';
4010 if (0 != dbDoSomething(acntsI, FALSE,
4011 first,
4012 last,
4013 getStrH(Rd->stuff, "email"),
4014 (strcmp("", getStrH(Rd->stuff, "created")) != 0) ? atoi(getStrH(Rd->stuff, "created")) : (int) Rd->shs.timeStamp[1].tv_sec,
4015 uuid,
4016 "00000000-0000-0000-0000-000000000000",
4017 atoi(level),
4018 0,
4019 getLevel(atoi(level)),
4020// "", // Defaults to NULL, empty string seems OK to. Then gets filled in later.
4021 1
4022 ))
4023 bitch(Rd, "Internal error.", "Failed to create UserAccounts record.");
4024 else
4025 {
4026 char uuidI[37], uuidR[37], uuidC[37], uuidS[37];
4027 uuid_t binuuidI;
4028 int r = 0, i;
4029
4030 // Create inventory records.
4031 strcpy(uuidR, "00000000-0000-0000-0000-000000000000");
4032 for (i = 0; (NULL != sysFolders[i].name) && (0 == r); i++)
4033 {
4034 uuid_generate_random(binuuidI);
4035 uuid_unparse_lower(binuuidI, uuidI);
4036// TODO - should check there isn't a folder with this UUID already.
4037 D("Creating %s inventory folder for user %s.", sysFolders[i].name, getStrH(Rd->stuff, "name"));
4038 r += dbDoSomething(invFolderI, FALSE, uuid, sysFolders[i].name, sysFolders[i].type, 1, uuidI, uuidR); // LEAKY
4039 if (0 != r)
4040 bitch(Rd, "Internal error.", "Failed to create invenoryFolder record.");
4041 if (strcmp("My Inventory", sysFolders[i].name) == 0)
4042 strcpy(uuidR, uuidI);
4043 if (strcmp("Calling Cards", sysFolders[i].name) == 0)
4044 strcpy(uuidC, uuidI);
4045 if (strcmp("My Suitcase", sysFolders[i].name) == 0)
4046 strcpy(uuidS, uuidI);
4047 }
4048
4049 uuid_generate_random(binuuidI);
4050 uuid_unparse_lower(binuuidI, uuidI);
4051// TODO - should check there isn't a folder with this UUID already.
4052 D("Creating %s inventory folder for user %s.", "Friends", getStrH(Rd->stuff, "name"));
4053 r += dbDoSomething(invFolderI, FALSE, uuid, "Friends", 2, 1, uuidI, uuidC);
4054 if (0 != r)
4055 bitch(Rd, "Internal error.", "Failed to create invenoryFolder record.");
4056 strcpy(uuidC, uuidI);
4057
4058 uuid_generate_random(binuuidI);
4059 uuid_unparse_lower(binuuidI, uuidI);
4060// TODO - should check there isn't a folder with this UUID already.
4061 D("Creating %s inventory folder for user %s.", "All", getStrH(Rd->stuff, "name"));
4062 r += dbDoSomething(invFolderI, FALSE, uuid, "All", 2, 1, uuidI, uuidC);
4063 if (0 != r)
4064 bitch(Rd, "Internal error.", "Failed to create invenoryFolder record.");
4065
4066 for (i = 1; (NULL != sysFolders[i].name) && (0 == r); i++)
4067 {
4068 uuid_generate_random(binuuidI);
4069 uuid_unparse_lower(binuuidI, uuidI);
4070// TODO - should check there isn't a folder with this UUID already.
4071 D("Creating %s inventory folder for user %s.", sysFolders[i].name, getStrH(Rd->stuff, "name"));
4072 r += dbDoSomething(invFolderI, FALSE, uuid, sysFolders[i].name, sysFolders[i].type, 1, uuidI, uuidS);
4073 if (0 != r)
4074 bitch(Rd, "Internal error.", "Failed to create invenoryFolder record.");
4075 }
4076
4077 if (0 == r)
4078 {
4079 // Create location record.
4080 D("Creating home and last positions for user %s.", getStrH(Rd->stuff, "name"));
4081 if (0 != dbDoSomething(gUserI, FALSE, uuid)) // LEAKY
4082 bitch(Rd, "Internal error.", "Failed to create GridUser record.");
4083 else
4084 {
4085 // Finally create auth record, so they can log in.
4086 D("Creating auth record for user %s %s.", uuid, getStrH(Rd->stuff, "name"));
4087 if (0 != dbDoSomething(authI, FALSE, uuid, getStrH(Rd->stuff, "passwordSalt"), getStrH(Rd->stuff, "passwordHash"), "UserAccount", "00000000-0000-0000-0000-000000000000"))
4088 bitch(Rd, "Internal error.", "Failed to create auth record.");
4089 }
4090 }
4091
4092 // load iar -m first last / password /opt/opensim_SC/backups/DefaultMember.IAR
4093 simList *sims = getSims();
4094 struct sysinfo info;
4095 float la;
4096
4097 sysinfo(&info);
4098 la = info.loads[0]/65536.0;
4099
4100 for (i = 0; i < sims->num; i++)
4101 {
4102 char *sim = sims->sims[i], *name = getSimName(sims->sims[i]);
4103
4104 if (checkSimIsRunning(sim))
4105 {
4106 I("Loading default member IAR for %s %s in sim %s, this might take a couple of minutes.", first, last, name);
4107 char *c = xmprintf("%s %s/%s send-keys -t '%s:%d' 'load iar -m %s %s / password /opt/opensim_SC/backups/DefaultMember.IAR' Enter",
4108 Tcmd, scRun, Tsocket, Tconsole, i + 1, first, last);
4109T(c);
4110 int r = system(c);
4111 if (!WIFEXITED(r))
4112 E("tmux load iar command failed!");
4113 else
4114 {
4115// memset(toybuf, 0, sizeof(toybuf));
4116// snprintf(toybuf, sizeof(toybuf), "INITIALIZATION COMPLETE FOR %s", name);
4117// waitTmuxText(name, toybuf);
4118// I("%s is done starting up.", name);
4119// la = waitLoadAverage(la, loadAverageInc, simTimeOut);
4120 }
4121 free(c);
4122 free(name);
4123 break;
4124 }
4125 free(name);
4126 }
4127 freeSimList(sims);
4128 }
4129 free(first);
4130 }
4131 }
4132 }
4133 else
4134 W("Not writing NULL UUID user!");
4135 free(file);
4136}
4137
4138static sesh *newSesh(reqData *Rd, boolean linky)
4139{
4140 unsigned char *md5hash = xzalloc(17);
4141 char *toke_n_munchie, *munchie, *hashish, *t0, *t1;
4142 char uuid[37];
4143 uuid_t binuuid;
4144 sesh *ret = &Rd->shs;
4145
4146T("new sesh %s %s %s", linky ? "linky" : "session", ret->UUID, ret->name);
4147 if (linky)
4148 {
4149 Rd->lnk = xzalloc(sizeof(sesh));
4150 ret = Rd->lnk;
4151 ret->UUID = Rd->shs.UUID;
4152 }
4153
4154 char buf[128]; // 512 bits.
4155 int numBytes = getrandom((void *)buf, sizeof(buf), GRND_NONBLOCK);
4156
4157 // NOTE that getrandom() returns random bytes, which may include '\0'.
4158 if (-1 == numBytes)
4159 {
4160 perror_msg("Unable to generate a suitable random number.");
4161 // EAGAIN - not enough entropy, try again.
4162 // EINTR - signal handler interrupted it, try again.
4163 }
4164 else
4165 {
4166 t0 = qhex_encode(buf, sizeof(buf));
4167 qstrcpy(ret->salt, sizeof(ret->salt), t0);
4168 free(t0);
4169//d("salt %s", ret->salt);
4170 numBytes = getrandom((void *)buf, sizeof(buf), GRND_NONBLOCK);
4171 if (-1 == numBytes)
4172 perror_msg("Unable to generate a suitable random number.");
4173 else
4174 {
4175 t0 = qhex_encode(buf, sizeof(buf));
4176 qstrcpy(ret->seshID, sizeof(ret->seshID), t0);
4177 free(t0);
4178//d("seshID %s", ret->seshID);
4179
4180 ret->timeStamp[0].tv_nsec = UTIME_OMIT;
4181 ret->timeStamp[0].tv_sec = UTIME_OMIT;
4182 if (-1 == clock_gettime(CLOCK_REALTIME, &ret->timeStamp[1]))
4183 perror_msg("Unable to get the time.");
4184 else
4185 {
4186 // tv_sec is a time_t, tv_nsec is a long, but the actual type of time_t isn't well defined, it's some sort of integer.
4187 t0 = xmprintf("%s%ld.%ld", ret->seshID, (long) ret->timeStamp[1].tv_sec, ret->timeStamp[1].tv_nsec);
4188 qstrcpy(ret->sesh, sizeof(ret->sesh), t0);
4189//d("sesh %s", ret->sesh);
4190 t1 = myHMAC(t0, FALSE);
4191 free(t0);
4192 munchie = xmprintf("%s%ld.%ld", t1, (long) ret->timeStamp[1].tv_sec, ret->timeStamp[1].tv_nsec);
4193 free(t1);
4194 qstrcpy(ret->munchie, sizeof(ret->munchie), munchie);
4195//d("munchie %s", ret->munchie);
4196// TODO - chicken and egg? Used to be from stuff->UUID.
4197 t1 = ret->UUID;
4198 if (NULL == t1)
4199 {
4200 uuid_clear(binuuid);
4201 uuid_unparse_lower(binuuid, uuid);
4202 ret->UUID = xstrdup(uuid);
4203 }
4204 t0 = xmprintf("%s%s", ret->UUID, munchie);
4205 free(munchie);
4206 toke_n_munchie = myHMAC(t0, FALSE);
4207 free(t0);
4208 qstrcpy(ret->toke_n_munchie, sizeof(ret->toke_n_munchie), toke_n_munchie);
4209//d("toke_n_munchie %s", ret->toke_n_munchie);
4210 hashish = myHMACkey(ret->salt, toke_n_munchie, FALSE);
4211 free(toke_n_munchie);
4212 qstrcpy(ret->hashish, sizeof(ret->hashish), hashish);
4213//d("hashish %s", ret->hashish);
4214 t0 = myHMACkey(getStrH(Rd->configs, "pepper"), hashish, TRUE);
4215 free(hashish);
4216 qstrcpy(ret->leaf, sizeof(ret->leaf), t0);
4217//d("leaf %s", ret->leaf);
4218 free(t0);
4219 ret->isLinky = linky;
4220 setToken_n_munchie(Rd, linky);
4221 }
4222 }
4223 }
4224
4225 free(md5hash);
4226 return ret;
4227}
4228
4229
4230
4231
4232/* CRUD (Create, Read, Update, Delete)
4233CRAP (Create, Replicate, Append, Process)
4234Though I prefer -
4235DAVE (Delete, Add, View, Edit), coz the names are shorter. B-)
4236On the other hand, list or browse needs to be added, which is why they have
4237BREAD (Browse, Read, Edit, Add, Delete)
4238CRUDL (Create, Read, Update, Delete, List)
4239CRUDE (Create, Read, Update, Delete, Experience)
4240Maybe -
4241DAVEE (Delete, Add, View, Edit, Explore)
4242*/
4243
4244// lua.h has LUA_T* NONE, NIL, BOOLEAN, LIGHTUSERDATA, NUMBER, STRING, TABLE, FUNCTION, USERDATA, THREAD as defines, -1 - 8.
4245// These are the missing ones. Then later we will have prim, mesh, script, sound, terrain, ...
4246#define LUA_TGROUP 42
4247#define LUA_TINTEGER 43
4248#define LUA_TEMAIL 44
4249#define LUA_TPASSWORD 45
4250#define LUA_TFILE 46
4251#define LUA_TIMAGE 47
4252
4253#define FLD_NONE 0
4254#define FLD_EDITABLE 1
4255#define FLD_HIDDEN 2
4256#define FLD_REQUIRED 4
4257
4258typedef struct _inputField inputField;
4259typedef struct _inputSub inputSub;
4260typedef struct _inputForm inputForm;
4261typedef struct _inputValue inputValue;
4262
4263typedef int (*inputFieldValidFunc) (reqData *Rd, inputForm *iF, inputValue *iV);
4264typedef void (*inputFieldShowFunc) (reqData *Rd, inputForm *iF, inputValue *iV);
4265typedef int (*inputSubmitFunc) (reqData *Rd, inputForm *iF, inputValue *iV);
4266typedef void (*inputFormShowFunc) (reqData *Rd, inputForm *iF, inputValue *iV);
4267
4268struct _inputField
4269{
4270 char *name, *title, *help;
4271 inputFieldValidFunc validate; // Alas C doesn't have any anonymous function standard.
4272 inputFieldShowFunc web, console, gui;
4273 inputField **group; // If this is a LUA_TGROUP, then this will be a null terminated array of the fields in the group.
4274// database details
4275// lua file details
4276 signed char type, flags;
4277 short editLevel, viewLevel, viewLength, maxLength;
4278};
4279struct _inputSub
4280{
4281 char *name, *title, *help, *outputForm;
4282 inputSubmitFunc submit;
4283};
4284struct _inputForm
4285{
4286 char *name, *title, *help;
4287 qlisttbl_t *fields; // qlisttbl coz iteration in order and lookup are important.
4288 qhashtbl_t *subs;
4289 inputFormShowFunc web, eWeb; // display web, console, gui;
4290// read function
4291// write function
4292};
4293struct _inputValue
4294{
4295 inputField *field;
4296 void *value; // If this is a LUA_TGROUP, then this will be a null.
4297 short valid; // 0 for not yet validated, negative for invalid, positive for valid.
4298 short source, index;
4299};
4300
4301
4302static int sessionValidate(reqData *Rd, inputForm *iF, inputValue *iV)
4303{
4304 int ret = 0;
4305 boolean linky = FALSE;
4306 char *toke_n_munchie = "", *munchie = "", *hashish = "", *leaf = "", *timeStamp = "", *seshion = "", *seshID = "", *t0, *t1;
4307
4308 // In this case the session stuff has to come from specific places.
4309 hashish = Rd->queries->getstr(Rd->queries, "hashish", true);
4310//d("O hashish %s", hashish);
4311 if (NULL != hashish)
4312 {
4313 char *t = xstrdup(hashish);
4314 size_t sz = qB64_decode(t);
4315
4316// TODO - should validate the cookie version as well, if it was sent.
4317// Coz it later tries to delete the linky as if it was the cookie session, and might give us a chance to delete the old session.
4318// Though only if there's a munchie in the body?
4319 I("Validating LINKY hashish %s", hashish);
4320 free(hashish);
4321 hashish = qhex_encode(t, sz);
4322 free(t);
4323 linky = TRUE;
4324 }
4325 else
4326 {
4327 toke_n_munchie = getStrH(Rd->cookies, "toke_n_munchie");
4328// munchie = getStrH(Rd->body, "munchie");
4329 hashish = Rd->cookies->getstr(Rd->cookies, "hashish", true);
4330 if (('\0' == toke_n_munchie[0]) || ((NULL == hashish)))
4331 {
4332 if (strcmp("logout", Rd->doit) == 0)
4333 {
4334 I("Not checking session, coz we are logging out.");
4335 Rd->shs.status = SHS_NUKE;
4336 return ret;
4337 }
4338 bitchSession(Rd, "Invalid session.", "No or blank hashish or toke_n_munchie.");
4339 Rd->shs.status = SHS_NONE;
4340 ret++;
4341 }
4342 else
4343 I("Validating SESSION hashish %s", hashish);
4344 }
4345
4346//d("O toke_n_munchie %s", toke_n_munchie);
4347//d("O munchie %s", munchie);
4348 if (0 == ret)
4349 {
4350 struct stat st;
4351 struct timespec now;
4352
4353 leaf = myHMACkey(getStrH(Rd->configs, "pepper"), hashish, TRUE);
4354//d("leaf %s", leaf);
4355 if (linky)
4356 t0 = xmprintf("%s/sessions/%s.linky", scCache, leaf);
4357 else
4358 t0 = xmprintf("%s/sessions/%s.lua", scCache, leaf);
4359
4360 qhashtbl_t *tnm = qhashtbl(0, 0);
4361 ret = LuaToHash(Rd, t0, "toke_n_munchie", tnm, ret, &st, &now, "session");
4362 free(t0);
4363
4364 if (0 != ret)
4365 {
4366 // This might be coz it's a stale session that was deleted already, so shouldn't complain really if they are just getting the login page.
4367 // They might also have a stale doit and form cookie.
4368// bitchSession(Rd, "Invalid session.", "No session file.");
4369 bitchSession(Rd, "", "No session file.");
4370 ret++;
4371 }
4372 else
4373 {
4374 // This is apparently controversial, I added it coz some of the various security docs suggested it's a good idea.
4375 // https://security.stackexchange.com/questions/139952/why-arent-sessions-exclusive-to-an-ip-address?rq=1
4376 // Includes various reasons why it's bad.
4377 // Another good reason why it is bad, TOR.
4378 // So should make this a user option, like Mantis does.
4379 if (strcmp(getStrH(Rd->headers, "REMOTE_ADDR"), getStrH(tnm, "IP")) != 0)
4380 {
4381 bitchSession(Rd, "Wrong IP for session.", "Session IP doesn't match.");
4382 ret++;
4383 }
4384 else
4385 {
4386 timeStamp = xmprintf("%ld.%ld", (long) st.st_mtim.tv_sec, st.st_mtim.tv_nsec);
4387//d("timeStamp %s", timeStamp);
4388 seshion = xmprintf("%s%s", tnm->getstr(tnm, "seshID", false), timeStamp);
4389//d("sesh %s", seshion);
4390 t0 = myHMAC(seshion, FALSE);
4391 munchie = xmprintf("%s%s", t0, timeStamp);
4392//d("munchie %s", munchie);
4393 free(t0);
4394 free(timeStamp);
4395 t1 = getStrH(Rd->body, "munchie");
4396 if ('\0' != t1[0])
4397 {
4398 if (strcmp(t1, munchie) != 0)
4399 {
4400// TODO if newbie user has not logged out, but clicks the email linky, and they end up on a new browser tab, they'll see this on the logged in tab.
4401 bitchSession(Rd, "Wrong munchie for session, may have been eaten, please try again.", "HMAC(seshID + timeStamp) != munchie");
4402 ret++;
4403 }
4404 else
4405 {
4406 t0 = xmprintf("%s%s", getStrH(tnm, "UUID"), munchie);
4407 t1 = myHMAC(t0, FALSE);
4408 free(t0);
4409
4410//d("toke_n_munchie %s", t1);
4411 if (strcmp(t1, toke_n_munchie) != 0)
4412 {
4413 bitchSession(Rd, "Wrong toke_n_munchie for session.", "HMAC(UUID + munchie) != toke_n_munchie");
4414 ret++;
4415 }
4416 free(t1);
4417 }
4418
4419 if (linky)
4420 {
4421 t0 = xmprintf("%s%s", getStrH(tnm, "UUID"), munchie);
4422 t1 = myHMAC(t0, FALSE);
4423 free(t0);
4424 toke_n_munchie = t1;
4425//d("toke_n_munchie %s", t1);
4426 }
4427 t1 = myHMACkey(getStrH(tnm, "salt"), toke_n_munchie, FALSE);
4428//d("hashish %s", t1);
4429 if (strcmp(t1, hashish) != 0)
4430 {
4431 bitchSession(Rd, "Wrong hashish for session.", "HMAC(toke_n_munchie + salt) != hashish");
4432 ret++;
4433 }
4434 free(t1);
4435 }
4436
4437// TODO - should carefully review all of this, especially the moving of session data to and fro.
4438 if (0 == ret)
4439 {
4440W("Validated session.");
4441 sesh *shs = &Rd->shs;
4442
4443 qstrcpy(shs->leaf, sizeof(shs->leaf), leaf);
4444 if (NULL != shs->name) free(shs->name);
4445 shs->name = tnm->getstr(tnm, "name", true);
4446 if (NULL != shs->UUID) free(shs->UUID);
4447 shs->UUID = tnm->getstr(tnm, "UUID", true);
4448 if (linky)
4449 {
4450W("Validated session linky.");
4451 addStrL(Rd->messages, "Congratulations, you have validated your new account. Now you can log onto the web site.");
4452 addStrL(Rd->messages, "NOTE - you wont be able to log onto the grid until your new account has been approved.");
4453 Rd->lnk = xzalloc(sizeof(sesh));
4454 Rd->lnk->status = SHS_NUKE;
4455 qstrcpy(Rd->lnk->leaf, sizeof(Rd->lnk->leaf), leaf);
4456 freeSesh(Rd, linky, FALSE);
4457 qstrcpy(Rd->lnk->leaf, sizeof(Rd->lnk->leaf), "");
4458 Rd->doit = "validate";
4459 Rd->output = "accountLogin";
4460 Rd->form = "accountLogin";
4461// TODO - we might want to delete their old .lua session as well. Maybe? Don't think we have any suitable codes to find it.
4462 }
4463 else
4464 {
4465 char *level = tnm->getstr(tnm, "level", false);
4466
4467 // Check for session timeouts etc.
4468 if (now.tv_sec > st.st_mtim.tv_sec + seshTimeOut)
4469 {
4470 bitch(Rd, "Session timed out.", "No activity for longer than seshTimeOut, session is ancient.");
4471 ret++;
4472 Rd->shs.status = SHS_ANCIENT;
4473 }
4474 else if (now.tv_sec > st.st_mtim.tv_sec + idleTimeOut)
4475 {
4476 bitch(Rd, "Session idled out.", "No activity for longer than idleTimeOut, session is idle.");
4477 ret++;
4478 Rd->shs.status = SHS_IDLE;
4479 }
4480 else if (now.tv_sec > st.st_mtim.tv_sec + seshRenew)
4481 {
4482 D("Session needs renewing.");
4483 Rd->shs.status = SHS_RENEW;
4484 }
4485 else
4486 Rd->shs.status = SHS_VALID;
4487
4488 if (NULL == level)
4489 level = "-256";
4490 qstrcpy(shs->sesh, sizeof(shs->sesh), seshion);
4491 qstrcpy(shs->toke_n_munchie, sizeof(shs->toke_n_munchie), toke_n_munchie);
4492 qstrcpy(shs->hashish, sizeof(shs->hashish), hashish);
4493 qstrcpy(shs->munchie, sizeof(shs->munchie), munchie);
4494 qstrcpy(shs->salt, sizeof(shs->salt), tnm->getstr(tnm, "salt", false));
4495 qstrcpy(shs->seshID, sizeof(shs->seshID), tnm->getstr(tnm, "seshID", false));
4496 shs->level = atoi(level);
4497// TODO - get level from somewhere and stuff it in shs.
4498 shs->timeStamp[0].tv_nsec = UTIME_OMIT;
4499 shs->timeStamp[0].tv_sec = UTIME_OMIT;
4500 memcpy(&shs->timeStamp[1], &st.st_mtim, sizeof(struct timespec));
4501 }
4502 }
4503
4504 qhashtbl_obj_t obj;
4505
4506 memset((void*)&obj, 0, sizeof(obj));
4507 tnm->lock(tnm);
4508 while(tnm->getnext(tnm, &obj, false) == true)
4509 {
4510 char *n = obj.name;
4511
4512 if ((strcmp("salt", n) != 0) && (strcmp("seshID", n) != 0) && (strcmp("UUID", n) != 0))
4513 {
4514t("SessionValidate() Lua read %s = %s", n, (char *) obj.data);
4515 Rd->stuff->putstr(Rd->stuff, obj.name, (char *) obj.data);
4516 }
4517 }
4518 tnm->unlock(tnm);
4519
4520// TODO - check this.
4521// Rd->database->putstr(Rd->database, "UserAccounts.PrincipalID", tnm->getstr(tnm, "UUID", false));
4522 }
4523 free(munchie);
4524 free(seshion);
4525 }
4526 free(leaf);
4527 tnm->free(tnm);
4528 free(hashish);
4529 }
4530
4531 return ret;
4532}
4533
4534static void sessionWeb(reqData *Rd, inputForm *iF, inputValue *iV)
4535{
4536 HTMLhidden(Rd->reply, iV->field->name, iV->value);
4537}
4538
4539/*
4540static int UUIDValidate(reqData *Rd, inputForm *iF, inputValue *iV)
4541{
4542 int ret = 0;
4543 char *UUID = (char *) iV->value;
4544
4545 if (36 != strlen(UUID))
4546 {
4547 bitch(Rd, "Internal error.", "UUID isn't long enough.");
4548 ret++;
4549 }
4550// TODO - check the characters and dashes as well.
4551
4552 if (0 == ret)
4553 Rd->stuff->putstr(Rd->stuff, "UUID", UUID);
4554 return ret;
4555}
4556
4557static void UUIDWeb(reqData *Rd, inputForm *iF, inputValue *iV)
4558{
4559 HTMLhidden(Rd->reply, iV->field->name, iV->value);
4560}
4561*/
4562
4563static int nameValidate(reqData *Rd, inputForm *iF, inputValue *iV)
4564{
4565 int ret = 0;
4566 unsigned char *name; // We have to be unsigned coz of isalnum().
4567 char *where = NULL;
4568
4569 name = xstrdup(iV->value);
4570
4571 if ((NULL == name) || ('\0' == name[0]))
4572 {
4573 bitch(Rd, "Please supply an account name.", "None supplied.");
4574 ret++;
4575 }
4576 else
4577 {
4578 int l0 = strlen(name), l1 = 0, l2 = 0;
4579
4580 if (0 == l0)
4581 {
4582 bitch(Rd, "Please supply an account name.", "Name is empty.");
4583 ret++;
4584 }
4585 else
4586 {
4587 int i;
4588 unsigned char *s = NULL;
4589
4590 for (i = 0; i < l0; i++)
4591 {
4592 if (isalnum(name[i]) == 0)
4593 {
4594
4595 if ((' ' == name[i] /*&& (NULL == s)*/))
4596 {
4597 s = &name[i];
4598 *s++ = '\0';
4599 while(' ' == *s)
4600 {
4601 i++;
4602 s++;
4603 }
4604 l1 = strlen(name);
4605 l2 = strlen(s);
4606
4607 // Apparently names are not case sensitive on login, but stored with case in the database.
4608 // I confirmed that, can log in no matter what case you use.
4609 // Seems to be good security for names to be case insensitive.
4610 // UserAccounts FirstName and LastName fields are both varchar(64) utf8_general_ci.
4611 // The MySQL docs say that the "_ci" bit means comparisons will be case insensitive. So that should work fine.
4612
4613 // SL docs say 31 characters each for first and last name. UserAccounts table is varchar(64) each. userinfo has varchar(50) for the combined name.
4614 // The userinfo table seems to be obsolete.
4615 // Singularity at least limits the total name to 64.
4616 // I can't find any limitations on characters allowed, but I only ever see letters and digits used. Case is stored, but not significant.
4617 // OpenSims "create user" console command doesn't sanitize it at all, even crashing on some names.
4618 }
4619 else
4620 {
4621 bitch(Rd, "First and last names are limited to ordinary letters and digits, no special characters or fonts.", "");
4622 ret++;
4623 break;
4624 }
4625// TODO - compare first, last, and fullname against god names, complain and fail if there's a match.
4626 }
4627 }
4628
4629 if (NULL == s)
4630 {
4631 bitch(Rd, "Account names have to be two words.", "");
4632 ret++;
4633 }
4634 if ((31 < l1) || (31 < l2))
4635 {
4636 bitch(Rd, "First and last names are limited to 31 letters each.", "");
4637 ret++;
4638 }
4639 if ((0 == l1) || (0 == l2))
4640 {
4641 bitch(Rd, "First and last names have to be one or more ordinary letters or digits each.", "");
4642 ret++;
4643 }
4644
4645 if (0 == ret)
4646 {
4647 Rd->stuff->putstr(Rd->stuff, "firstName", name);
4648 Rd->stuff->putstr(Rd->stuff, "lastName", s);
4649 Rd->stuff->putstrf(Rd->stuff, "name", "%s %s", name, s);
4650 }
4651 }
4652 }
4653 free(name);
4654
4655 return ret;
4656}
4657
4658static void nameWeb(reqData *Rd, inputForm *oF, inputValue *oV)
4659{
4660 if (oV->field->flags & FLD_HIDDEN)
4661 HTMLhidden(Rd->reply, oV->field->name, oV->value);
4662 else
4663 HTMLtext(Rd->reply, "text", oV->field->title, oV->field->name, oV->value, oV->field->viewLength, oV->field->maxLength, oV->field->flags & FLD_REQUIRED);
4664}
4665
4666
4667static int passwordValidate(reqData *Rd, inputForm *iF, inputValue *iV)
4668{
4669 int ret = 0;
4670 char *password = (char *) iV->value, *salt = getStrH(Rd->stuff, "passSalt"), *hash = getStrH(Rd->stuff, "passHash");
4671
4672 if ((NULL == password) || ('\0' == password[0]))
4673 {
4674 bitch(Rd, "Please supply a password.", "Password empty or missing.");
4675 ret++;
4676 }
4677 else if (('\0' != salt[0]) && ('\0' != hash[0]) && (strcmp("psswrd", iV->field->name) == 0))
4678 {
4679 D("Comparing passwords. %s %s %s", password, salt, hash);
4680 char *h = checkSLOSpassword(Rd, salt, password, hash, "Passwords are not the same.");
4681
4682 if (NULL == h)
4683 ret++;
4684 else
4685 free(h);
4686 }
4687
4688// TODO - once the password is validated, store it as the salt and hash.
4689// If it's an existing account, compare it? Or do that later?
4690 if (0 == ret)
4691 Rd->stuff->putstr(Rd->stuff, "password", password);
4692
4693 return ret;
4694}
4695
4696static void passwordWeb(reqData *Rd, inputForm *oF, inputValue *oV)
4697{
4698 HTMLtext(Rd->reply, "password", oV->field->title, oV->field->name, "", oV->field->viewLength, oV->field->maxLength, oV->field->flags & FLD_REQUIRED);
4699 Rd->reply->addstr(Rd->reply, "<p>While viewers will usually remember your name and password for you, you'll need to remember it for this web site to. &nbsp; "
4700 "I highly recommend using a password manager. &nbsp; KeePass and it's variations is a great password manager.</p>\n");
4701}
4702
4703static int emailValidate(reqData *Rd, inputForm *iF, inputValue *iV)
4704{
4705// inputField **group = iV->field->group;
4706 int ret = 0, i;
4707 boolean notSame = FALSE;
4708
4709 i = iV->index;
4710 if (2 == i)
4711 {
4712 char *email = (char *) iV->value;
4713 char *emayl = (char *) (iV + 1)->value;
4714
4715 if ((NULL == email) || (NULL == emayl) || ('\0' == email[0]) || ('\0' == emayl[0]))
4716 {
4717 bitch(Rd, "Please supply an email address.", "None supplied.");
4718 ret++;
4719 }
4720 else if (strcmp(email, emayl) != 0)
4721 {
4722 bitch(Rd, "Email addresses are not the same.", "");
4723 ret++;
4724 notSame = TRUE;
4725 }
4726 else if (!qstr_is_email(email))
4727 {
4728 bitch(Rd, "Please supply a proper email address.", "Failed qstr_is_email()");
4729 ret++;
4730 }
4731 else
4732 {
4733// TODO - do other email checks - does the domain exist, ..
4734 }
4735
4736 if ((NULL != email) && (NULL != emayl))
4737 {
4738 char *t0 = qurl_encode(email, strlen(email));
4739
4740 // In theory it's the correct thing to do to NOT load email into stuff on failure,
4741 // In practice, that means it wont show the old email and emayl in the create page when they don't match.
4742 if ((0 == ret) || notSame)
4743 Rd->stuff->putstrf(Rd->stuff, "email", "%s", t0);
4744 free(t0);
4745 }
4746 if ((NULL != email) && (NULL != emayl))
4747 {
4748 char *t1 = qurl_encode(emayl, strlen(emayl));
4749
4750 Rd->stuff->putstrf(Rd->stuff, "emayl", "%s", t1);
4751 free(t1);
4752 }
4753 }
4754
4755 return ret;
4756}
4757static void emailWeb(reqData *Rd, inputForm *oF, inputValue *oV)
4758{
4759 HTMLtext(Rd->reply, "email", oV->field->title, oV->field->name, getStrH(Rd->stuff, oV->field->name), oV->field->viewLength, oV->field->maxLength, oV->field->flags & FLD_REQUIRED);
4760 Rd->reply->addstrf(Rd->reply, "<p>An email will be sent from %s@%s, and it might be in your spam folder, coz these sorts of emails sometimes end up there. &nbsp; "
4761 "You should add that email address to your contacts, or otherwise let it through your spam filter.</p>",
4762 "grid_no_reply", Rd->Host);
4763}
4764
4765
4766char *months[] =
4767{
4768 "january",
4769 "february",
4770 "march",
4771 "april",
4772 "may",
4773 "june",
4774 "july",
4775 "august",
4776 "september",
4777 "october",
4778 "november",
4779 "december"
4780};
4781static int DoBValidate(reqData *Rd, inputForm *iF, inputValue *iV)
4782{
4783 int ret = 0, i;
4784 char *t0, *t1;
4785// inputField **group = iV->field->group;
4786
4787 i = iV->index;
4788 if (2 == i)
4789 {
4790 t0 = (char *) iV->value;
4791 if ((NULL == t0) || ('\0' == t0[0]))
4792 {
4793 bitch(Rd, "Please supply a year of birth.", "None supplied.");
4794 ret++;
4795 }
4796 else
4797 {
4798 i = atoi(t0);
4799// TODO - get this to use current year instead of 2020.
4800 if ((1900 > i) || (i > 2020))
4801 {
4802 bitch(Rd, "Please supply a year of birth.", "Out of range.");
4803 ret++;
4804 }
4805 else if (i < 1901)
4806 {
4807 bitch(Rd, "Please supply a proper year of birth.", "Out of range, too old.");
4808 ret++;
4809 }
4810 else if (i >2004)
4811 {
4812 bitch(Rd, "This grid is Adult rated, you are too young.", "Out of range, too young.");
4813 ret++;
4814 }
4815 }
4816 t1 = (char *) (iV + 1)->value;
4817 if ((NULL == t1) || ('\0' == t1[0]))
4818 {
4819 bitch(Rd, "Please supply a month of birth.", "None supplied.");
4820 ret++;
4821 }
4822 else
4823 {
4824 for (i = 0; i < 12; i++)
4825 {
4826 if (strcmp(months[i], t1) == 0)
4827 break;
4828 }
4829 if (12 == i)
4830 {
4831 bitch(Rd, "Please supply a month of birth.", "Out of range");
4832 ret++;
4833 }
4834 }
4835
4836 if (0 == ret)
4837 {
4838 Rd->stuff->putstr(Rd->stuff, "year", t0);
4839 Rd->stuff->putstr(Rd->stuff, "month", t1);
4840 Rd->stuff->putstrf(Rd->stuff, "DoB", "%s %s", t0, t1);
4841 }
4842 }
4843
4844 return ret;
4845}
4846static void DoByWeb(reqData *Rd, inputForm *oF, inputValue *oV)
4847{
4848 char *tmp = xmalloc(16), *t;
4849 int i, d;
4850
4851 Rd->reply->addstr(Rd->reply, "<label>Date of birth :<table><tr><td>\n");
4852 HTMLselect(Rd->reply, NULL, oV->field->name);
4853 t = getStrH(Rd->stuff, "year");
4854 if (NULL == t)
4855 d = -1;
4856 else
4857 d = atoi(t);
4858 HTMLoption(Rd->reply, "", FALSE);
4859 for (i = 1900; i <= 2020; i++)
4860 {
4861 boolean sel = FALSE;
4862
4863 if (i == d)
4864 sel = TRUE;
4865 sprintf(tmp, "%d", i);
4866 HTMLoption(Rd->reply, tmp, sel);
4867 }
4868 free(tmp);
4869 HTMLselectEndNo(Rd->reply);
4870}
4871static void DoBmWeb(reqData *Rd, inputForm *oF, inputValue *oV)
4872{
4873 char *t;
4874 int i, d;
4875
4876 Rd->reply->addstr(Rd->reply, "</td><td>\n");
4877 HTMLselect(Rd->reply, NULL, oV->field->name);
4878 t = getStrH(Rd->stuff, "month");
4879 HTMLoption(Rd->reply, "", FALSE);
4880 for (i = 0; i <= 11; i++)
4881 {
4882 boolean sel = FALSE;
4883
4884 if ((NULL != t) && (strcmp(t, months[i]) == 0))
4885 sel = TRUE;
4886 HTMLoption(Rd->reply, months[i], sel);
4887 }
4888 HTMLselectEndNo(Rd->reply);
4889 Rd->reply->addstr(Rd->reply, "</td></tr></table></label>\n");
4890}
4891static void DoBWeb(reqData *Rd, inputForm *oF, inputValue *oV)
4892{
4893}
4894
4895static int legalValidate(reqData *Rd, inputForm *iF, inputValue *iV)
4896{
4897 int ret = 0, i;
4898 char *t;
4899 inputField **group = iV->field->group;
4900
4901 i = iV->index;
4902 if (2 == i)
4903 {
4904 t = (char *) iV->value;
4905 if ((NULL == t) || (strcmp("on", t) != 0))
4906 {
4907 bitch(Rd, "You must be an adult to enter this site.", "");
4908 ret++;
4909 }
4910 else
4911 Rd->stuff->putstr(Rd->stuff, "adult", t);
4912 t = (char *) (iV + 1)->value;
4913 if ((NULL == t) || (strcmp("on", t) != 0))
4914 {
4915 bitch(Rd, "You must agree to the Terms & Conditions of Use.", "");
4916 ret++;
4917 }
4918 else
4919 Rd->stuff->putstr(Rd->stuff, "agree", t);
4920 }
4921
4922 return ret;
4923}
4924static void adultWeb(reqData *Rd, inputForm *oF, inputValue *oV)
4925{
4926 HTMLcheckBox(Rd->reply, oV->field->name, oV->field->title, !strcmp("on", getStrH(Rd->body, "adult")), oV->field->flags & FLD_REQUIRED);
4927}
4928static void agreeWeb(reqData *Rd, inputForm *oF, inputValue *oV)
4929{
4930 HTMLcheckBox(Rd->reply, oV->field->name, oV->field->title, !strcmp("on", getStrH(Rd->body, "agree")), oV->field->flags & FLD_REQUIRED);
4931}
4932static void legalWeb(reqData *Rd, inputForm *oF, inputValue *oV)
4933{
4934}
4935static void ToSWeb(reqData *Rd, inputForm *oF, inputValue *oV)
4936{
4937 Rd->reply->addstrf(Rd->reply, "<h2>Terms of Service</h2><pre>%s</pre>\n", getStrH(Rd->configs, "ToS"));
4938}
4939
4940static int voucherValidate(reqData *Rd, inputForm *oF, inputValue *oV)
4941{
4942 int ret = 0;
4943 char *voucher = (char *) oV->value;
4944
4945 if ((NULL == voucher) || ('\0' == voucher[0]))
4946 {
4947 bitch(Rd, "Please fill in the 'Voucher' section.", "None supplied.");
4948 ret++;
4949 }
4950
4951 if ((0 == ret) && (NULL != voucher))
4952 Rd->stuff->putstr(Rd->stuff, "voucher", voucher);
4953
4954 return ret;
4955}
4956static void voucherWeb(reqData *Rd, inputForm *oF, inputValue *oV)
4957{
4958 HTMLtext(Rd->reply, "text", oV->field->title, oV->field->name, oV->value, oV->field->viewLength, oV->field->maxLength, oV->field->flags & FLD_REQUIRED);
4959}
4960
4961static int aboutMeValidate(reqData *Rd, inputForm *oF, inputValue *oV)
4962{
4963 int ret = 0;
4964 char *about = (char *) oV->value;
4965
4966 if ((NULL == about) || ('\0' == about[0]))
4967 {
4968 bitch(Rd, "Please fill in the 'About me' section.", "None supplied.");
4969 ret++;
4970 }
4971
4972 if ((0 == ret) && (NULL != about))
4973 Rd->stuff->putstr(Rd->stuff, "aboutMe", about);
4974
4975 return ret;
4976}
4977
4978static void aboutMeWeb(reqData *Rd, inputForm *oF, inputValue *oV)
4979{
4980 // For maxlength - the MySQL database field is type text, which has a max length of 64 Kilobytes byets, but characters might take up 1 - 4 bytes, and maxlength is in characters.
4981 // For rows and cols, seems a bit broken, I ask for 5/42, I get 6,36. In world it seems to be 7,46
4982// TODO - check against the limit for in world profiles, coz this will become that.
4983// TODO - validate aboutMe, it should not be empty, and should not be longer than 64 kilobytes.
4984 HTMLtextArea(Rd->reply, oV->field->name, oV->field->title, 7, oV->field->viewLength, 4, oV->field->maxLength, "Describe yourself here.", "off", "true", "soft", oV->value, FALSE, FALSE);
4985}
4986
4987static void accountWebHeaders(reqData *Rd, inputForm *oF) //, char *name)
4988{
4989 char *linky = checkLinky(Rd);
4990
4991 HTMLheader(Rd->reply, "<!--#echo var=\"grid\" --> account manager");
4992 Rd->reply->addstrf(Rd->reply, "<h1><!--#echo var=\"grid\" --> account manager</h1>\n");
4993 if (NULL != Rd->shs.name)
4994 {
4995 char *nm = qstrreplace("tr", xstrdup(Rd->shs.name), " ", "+");
4996
4997 Rd->reply->addstrf(Rd->reply, "<h3>You are <a href='https://%s%s%s?user=%s'>%s</a></h3>\n", Rd->Host, Rd->Script, Rd->Path, nm, Rd->shs.name);
4998 Rd->reply->addstr(Rd->reply, linky);
4999 free(nm);
5000 }
5001 free(linky);
5002 if (0 != Rd->errors->size(Rd->messages))
5003 HTMLlist(Rd->reply, "messages -", Rd->messages);
5004 if (NULL != oF->help)
5005 Rd->reply->addstrf(Rd->reply, "<p>%s</p>\n", oF->help);
5006 HTMLform(Rd->reply, "", Rd->shs.munchie);
5007 HTMLhidden(Rd->reply, "form", oF->name);
5008}
5009
5010static void accountWebFields(reqData *Rd, inputForm *oF, inputValue *oV)
5011{
5012 int count = oF->fields->size(oF->fields), i;
5013
5014 for (i = 0; i < count; i++)
5015 {
5016 if (NULL != oV[i].field->web)
5017 oV[i].field->web(Rd, oF, &oV[i]);
5018 if ((NULL != oV[i].field->help) && ('\0' != oV[i].field->help[0]))
5019 Rd->reply->addstrf(Rd->reply, "<p>%s</p>\n", oV[i].field->help);
5020//d("accountWebFeilds(%s, %s)", oF->name, oV[i].field->name);
5021 }
5022}
5023
5024static void accountWebSubs(reqData *Rd, inputForm *oF)
5025{
5026 qhashtbl_obj_t obj;
5027
5028 Rd->reply->addstrf(Rd->reply, "<input type='submit' disabled style='display: none' aria-hidden='true' />\n"); // Stop Enter key on text fields triggering the first submit button.
5029 memset((void*)&obj, 0, sizeof(obj)); // must be cleared before call
5030 oF->subs->lock(oF->subs);
5031 while(oF->subs->getnext(oF->subs, &obj, false) == true)
5032 {
5033 inputSub *sub = (inputSub *) obj.data;
5034 if ('\0' != sub->title[0])
5035 HTMLbutton(Rd->reply, sub->name, sub->title);
5036//d("accountWebSubs(%s, %s '%s')", oF->name, sub->name, sub->title);
5037 }
5038 oF->subs->unlock(oF->subs);
5039}
5040
5041static void accountWebFooter(reqData *Rd, inputForm *oF)
5042{
5043 if (0 != Rd->errors->size(Rd->errors))
5044 HTMLlist(Rd->reply, "errors -", Rd->errors);
5045 HTMLformEnd(Rd->reply);
5046 HTMLfooter(Rd->reply);
5047}
5048
5049static void accountAddWeb(reqData *Rd, inputForm *oF, inputValue *oV)
5050{
5051 accountWebHeaders(Rd, oF);
5052 accountWebFields(Rd, oF, oV);
5053 accountWebSubs(Rd, oF);
5054 accountWebFooter(Rd, oF);
5055}
5056
5057static void accountLoginWeb(reqData *Rd, inputForm *oF, inputValue *oV)
5058{
5059 if (NULL != Rd->shs.name) free(Rd->shs.name);
5060 Rd->shs.name = NULL;
5061 if (NULL != Rd->shs.UUID) free(Rd->shs.UUID);
5062 Rd->shs.UUID = NULL;
5063 accountWebHeaders(Rd, oF);
5064 accountWebFields(Rd, oF, oV);
5065 accountWebSubs(Rd, oF);
5066 accountWebFooter(Rd, oF);
5067}
5068
5069// TODO - accountViewWeb() and accountViewWeb() should view and edit arbitrary accounts the user is not logged in as,
5070// but limit things based on being that viewed / edited account, and the users level.
5071static void accountViewWeb(reqData *Rd, inputForm *oF, inputValue *oV)
5072{
5073 char *name = getStrH(Rd->database, "Lua.name"),
5074 *level = getStrH(Rd->database, "UserAccounts.UserLevel"),
5075 *email = getStrH(Rd->database, "UserAccounts.Email"),
5076 *voucher = getStrH(Rd->database, "Lua.voucher"),
5077 *about = getStrH(Rd->database, "Lua.aboutMe");
5078 time_t crtd = atol(getStrH(Rd->database, "UserAccounts.Created"));
5079
5080 accountWebHeaders(Rd, oF);
5081 accountWebFields(Rd, oF, oV);
5082 Rd->reply->addstrf(Rd->reply, "<p><font size='5'><span style='font-size: x-large'><b>Name :</b></span></font> %s</p>", name);
5083 Rd->reply->addstrf(Rd->reply, "<p><font size='5'><span style='font-size: x-large'><b>Title / level :</b></span></font> %s / %s</p>", getLevel(atoi(level)), level);
5084 Rd->reply->addstrf(Rd->reply, "<p><font size='5'><span style='font-size: x-large'><b>Date of birth :</b></span></font> %s</p>", getStrH(Rd->database, "Lua.DoB"));
5085 Rd->reply->addstrf(Rd->reply, "<p><font size='5'><span style='font-size: x-large'><b>Created :</b></span></font> %s</p>", ctime(&crtd));
5086 Rd->reply->addstrf(Rd->reply, "<p><font size='5'><span style='font-size: x-large'><b>Email :</b></span></font> %s</p>", email);
5087 Rd->reply->addstrf(Rd->reply, "<p><font size='5'><span style='font-size: x-large'><b>UUID :</b></span></font> %s</p>", getStrH(Rd->database, "UserAccounts.PrincipalID"));
5088 Rd->reply->addstrf(Rd->reply, "<p><font size='5'><span style='font-size: x-large'><b>Voucher :</b></span></font> %s</p>", voucher);
5089 HTMLtextArea(Rd->reply, "aboutMe", "About", 7, 50, 4, 16384, "", "off", "true", "soft", about, FALSE, TRUE);
5090 accountWebSubs(Rd, oF);
5091 accountWebFooter(Rd, oF);
5092}
5093
5094static void accountEditWeb(reqData *Rd, inputForm *oF, inputValue *oV)
5095{
5096 char *name = getStrH(Rd->database, "Lua.name"),
5097 *level = getStrH(Rd->database, "UserAccounts.UserLevel"),
5098 *email = getStrH(Rd->database, "UserAccounts.Email"),
5099 *voucher = getStrH(Rd->database, "Lua.voucher"),
5100 *about = getStrH(Rd->database, "Lua.aboutMe"),
5101 *lvl = getLevel(atoi(level));
5102 short lv = atoi(level);
5103
5104 accountWebHeaders(Rd, oF);
5105 accountWebFields(Rd, oF, oV);
5106// HTMLtext(Rd->reply, "password", "Old password", "password", "", 16, 0, FALSE);
5107// Rd->reply->addstr(Rd->reply, "<p>Warning, the limit on password length is set by your viewer, some can't handle longer than 16 characters.</p>\n");
5108//// HTMLtext(Rd->reply, "title", "text", "title", getStrH(Rh->stuff, "title"), 16, 64, TRUE);
5109
5110 HTMLhidden(Rd->reply, "user", name);
5111 Rd->reply->addstrf(Rd->reply, "<p><font size='5'><span style='font-size: x-large'><b>Name :</b></span></font> %s</p>", name);
5112// Rd->reply->addstrf(Rd->reply, "<p><font size='5'><span style='font-size: x-large'><b>Email :</b></span></font> %s</p>", email);
5113 HTMLtextArea(Rd->reply, "aboutMe", "About", 7, 50, 4, 16384, "", "off", "true", "soft", about, FALSE, TRUE);
5114 Rd->reply->addstrf(Rd->reply, "<p><font size='5'><span style='font-size: x-large'><b>Voucher :</b></span></font> %s</p>", voucher);
5115
5116 if (200 <= Rd->shs.level)
5117 {
5118 qlisttbl_obj_t obj;
5119
5120 HTMLselect(Rd->reply, "level", "level");
5121 memset((void*)&obj, 0, sizeof(obj)); // must be cleared before call
5122 accountLevels->lock(accountLevels);
5123 while(accountLevels->getnext(accountLevels, &obj, NULL, false) == true)
5124 {
5125 boolean is = false;
5126 short l = atoi((char *) obj.name);
5127
5128 if (strcmp(lvl, (char *) obj.data) == 0)
5129 is = true;
5130
5131// if ((is) || ((l <= Rd->shs.level) && (l != -200) && (l != -100) && (l != -50))) // Not above our pay grade, not newbie, validated, nor vouched for.
5132 if ((is) || ((l <= Rd->shs.level) && (lv <= l))) // As per discussions, can't lower level. Do that in the console.
5133 HTMLoption(Rd->reply, (char *) obj.data, is);
5134 }
5135 accountLevels->unlock(accountLevels);
5136 HTMLselectEnd(Rd->reply);
5137
5138 Rd->reply->addstrf(Rd->reply, "<p><dl>");
5139 Rd->reply->addstrf(Rd->reply, "<dt>disabled</dt><dd>Account cannot log in anywhere.</dd>");
5140 Rd->reply->addstrf(Rd->reply, "<dt>newbie</dt><dd>Newly created account, not yet validated.</dd>");
5141 Rd->reply->addstrf(Rd->reply, "<dt>validated</dt><dd>Newly created account, they have clicked on the validation link in their validation email.</dd>");
5142 Rd->reply->addstrf(Rd->reply, "<dt>vouched for</dt><dd>Someone has vouched for this person.</dd>");
5143 Rd->reply->addstrf(Rd->reply, "<dt>approved</dt><dd>This person is approved, and can log into the world.</dd>");
5144 Rd->reply->addstrf(Rd->reply, "<dt>god</dt><dd>This is a god admin person.</dd>");
5145 Rd->reply->addstrf(Rd->reply, "</dl></p>");
5146 }
5147 else
5148 Rd->reply->addstrf(Rd->reply, "<p><font size='5'><span style='font-size: x-large'><b>Title / level :</b></span></font> %s / %s</p>", lvl, level);
5149
5150 accountWebSubs(Rd, oF);
5151 accountWebFooter(Rd, oF);
5152}
5153
5154
5155static int accountRead(reqData *Rd, char *uuid, char *firstName, char *lastName)
5156{
5157 int ret = 0, rt = -1;
5158 struct stat st;
5159 struct timespec now;
5160 qhashtbl_t *tnm = qhashtbl(0, 0);
5161 uuid_t binuuid;
5162 rowData *rows = NULL;
5163
5164 // Setup the database stuff.
5165 static dbRequest *uuids = NULL;
5166 if (NULL == uuids)
5167 {
5168 static char *szi[] = {"PrincipalID", NULL};
5169 static char *szo[] = {NULL};
5170 uuids = xzalloc(sizeof(dbRequest));
5171 uuids->table = "UserAccounts";
5172 uuids->inParams = szi;
5173 uuids->outParams = szo;
5174 uuids->where = "PrincipalID=?";
5175 dbRequests->addfirst(dbRequests, &uuids, sizeof(dbRequest *));
5176 }
5177 static dbRequest *acnts = NULL;
5178 if (NULL == acnts)
5179 {
5180 static char *szi[] = {"FirstName", "LastName", NULL};
5181 static char *szo[] = {NULL};
5182 acnts = xzalloc(sizeof(dbRequest));
5183 acnts->table = "UserAccounts";
5184 acnts->inParams = szi;
5185 acnts->outParams = szo;
5186 acnts->where = "FirstName=? and LastName=?";
5187 dbRequests->addfirst(dbRequests, &acnts, sizeof(dbRequest *));
5188 }
5189 static dbRequest *auth = NULL;
5190 if (NULL == auth)
5191 {
5192 static char *szi[] = {"UUID", NULL};
5193 static char *szo[] = {"passwordSalt", "passwordHash", NULL};
5194 auth = xzalloc(sizeof(dbRequest));
5195 auth->table = "auth";
5196 auth->inParams = szi;
5197 auth->outParams = szo;
5198 auth->where = "UUID=?";
5199 dbRequests->addfirst(dbRequests, &auth, sizeof(dbRequest *));
5200 }
5201
5202 Rd->fromDb = FALSE;
5203
5204// uuid = Rd->shs.UUID; first = getStrH(Rd->stuff, "firstName"); last = getStrH(Rd->stuff, "lastName");
5205
5206 // Special for showing another users details.
5207 if ('\0' != getStrH(Rd->queries, "user")[0])
5208 uuid = "";
5209
5210 char *first = xstrdup(""), *last = xstrdup("");
5211
5212 if (NULL != firstName)
5213 {
5214 free(first);
5215 first = xstrdup(firstName);
5216 if (NULL == lastName)
5217 {
5218 char *t = strchr(first, ' ');
5219
5220d("accountRead() single name |%s| |%s|", first, last);
5221 if (NULL == t)
5222 t = strchr(first, '+');
5223 if (NULL != t)
5224 {
5225 *t++ = '\0';
5226 free(last);
5227 last = xstrdup(t);
5228 }
5229 }
5230 else
5231 {
5232 free(last);
5233 last = xstrdup(lastName);
5234 }
5235 }
5236d("accountRead() UUID %s, name %s %s", uuid, first, last);
5237 uuid_clear(binuuid);
5238 if ((NULL != uuid) && ('\0' != uuid[0]))
5239 uuid_parse(uuid, binuuid);
5240 if ((NULL != uuid) && ('\0' != uuid[0]) && (!uuid_is_null(binuuid)))
5241 {
5242 char *where = xmprintf("%s/users/%s.lua", scData, uuid);
5243 rt = LuaToHash(Rd, where, "user", tnm, ret, &st, &now, "user");
5244
5245 free(where);
5246 dbDoSomething(uuids, FALSE, uuid);
5247 rows = uuids->rows;
5248 }
5249 else
5250 {
5251
5252 if ('\0' != first[0])
5253 {
5254 char *where = xmprintf("%s/users/%s_%s.lua", scData, first, last);
5255 rt = LuaToHash(Rd, where, "user", tnm, ret, &st, &now, "user");
5256
5257 free(where);
5258 dbDoSomething(acnts, FALSE, first, last); // LEAKY
5259 rows = acnts->rows;
5260 }
5261 }
5262// else
5263// {
5264// bitch(Rd, "Unable to read user record.", "Nothing available to look up a user record with.");
5265// rt = 1;
5266// }
5267
5268 if (0 == rt)
5269 {
5270T("Found Lua record.");
5271 ret += 1;
5272 Rd->database->putstr(Rd->database, "UserAccounts.FirstName", first);
5273 Rd->database->putstr(Rd->database, "UserAccounts.LastName", last);
5274 Rd->database->putstr(Rd->database, "UserAccounts.Email", getStrH(tnm, "email"));
5275 Rd->database->putstr(Rd->database, "UserAccounts.Created", getStrH(tnm, "created"));
5276 Rd->database->putstr(Rd->database, "UserAccounts.PrincipalID", getStrH(tnm, "UUID"));
5277 Rd->database->putstr(Rd->database, "UserAccounts.UserLevel", getStrH(tnm, "level"));
5278 Rd->database->putstr(Rd->database, "UserAccounts.UserFlags", getStrH(tnm, "flags"));
5279 Rd->database->putstr(Rd->database, "UserAccounts.UserTitle", getStrH(tnm, "title"));
5280 Rd->database->putstr(Rd->database, "UserAccounts.active", getStrH(tnm, "active"));
5281 Rd->database->putstr(Rd->database, "auth.passwordSalt", getStrH(tnm, "passwordSalt"));
5282 Rd->database->putstr(Rd->database, "auth.passwordHash", getStrH(tnm, "passwordHash"));
5283 Rd->stuff-> putstr(Rd->stuff, "linky-hashish", getStrH(tnm, "linky-hashish"));
5284 Rd->database->putstr(Rd->database, "Lua.name", getStrH(tnm, "name"));
5285 Rd->database->putstr(Rd->database, "Lua.DoB", getStrH(tnm, "DoB"));
5286 Rd->database->putstr(Rd->database, "Lua.agree", getStrH(tnm, "agree"));
5287 Rd->database->putstr(Rd->database, "Lua.adult", getStrH(tnm, "adult"));
5288 Rd->database->putstr(Rd->database, "Lua.aboutMe", getStrH(tnm, "aboutMe"));
5289 Rd->database->putstr(Rd->database, "Lua.vouched", getStrH(tnm, "vouched"));
5290 Rd->database->putstr(Rd->database, "Lua.voucher", getStrH(tnm, "voucher"));
5291 }
5292// else if (rows)
5293 if (rows)
5294 {
5295 rt = rows->rows->size(rows->rows);
5296 if (1 == rt)
5297 {
5298 ret = rt;
5299T("Found database record.");
5300 dbPull(Rd, "UserAccounts", rows);
5301
5302 char *name = xmprintf("%s %s", getStrH(Rd->database, "UserAccounts.FirstName"), getStrH(Rd->database, "UserAccounts.LastName"));
5303
5304 Rd->fromDb = TRUE;
5305 Rd->database->putstr(Rd->database, "Lua.name", name);
5306 free(name);
5307 dbDoSomething(auth, FALSE, getStrH(Rd->database, "UserAccounts.PrincipalID")); // LEAKY
5308 rows = auth->rows;
5309 if (rows)
5310 {
5311 if (1 == rows->rows->size(rows->rows))
5312 dbPull(Rd, "auth", rows);
5313 else
5314 {
5315 free(rows->fieldNames);
5316 rows->rows->free(rows->rows);
5317 free(rows);
5318 }
5319 }
5320 else
5321 {
5322 free(rows->fieldNames);
5323 rows->rows->free(rows->rows);
5324 free(rows);
5325 }
5326 }
5327 else
5328 {
5329 free(rows->fieldNames);
5330 rows->rows->free(rows->rows);
5331 free(rows);
5332 }
5333 }
5334 else
5335 {
5336 d("No user name or UUID to get an account for.");
5337 }
5338
5339 if (1 == ret)
5340 {
5341// TODO - this has to change when we are editing other peoples accounts.
5342 if ('\0' == getStrH(Rd->queries, "user")[0])
5343 {
5344// Rd->shs.level = atoi(getStrH(Rd->database, "UserAccounts.UserLevel"));
5345// TODO - might have to combine first and last here.
5346// Rd->shs.name = Rd->database->getstr(Rd->database, "Lua.name", true);
5347// Rd->shs.UUID = Rd->database->getstr(Rd->database, "UserAccounts.PrincipalID", true);
5348//d("accountRead() setting session uuid %s level %d name %s ", Rd->shs.UUID, (int) Rd->shs.level, Rd->shs.name);
5349 }
5350// Rd->stuff->putstr(Rd->stuff, "email", getStrH(Rd->database, "UserAccounts.Email"));
5351 }
5352
5353 free(last);
5354 free(first);
5355 tnm->free(tnm);
5356 return ret;
5357}
5358
5359static int accountDelSub(reqData *Rd, inputForm *iF, inputValue *iV)
5360{
5361 int ret = 0;
5362 char *uuid = Rd->shs.UUID, *first = getStrH(Rd->stuff, "firstName"), *last = getStrH(Rd->stuff, "lastName");
5363 int c = accountRead(Rd, uuid, first, last);
5364
5365 if (1 != c)
5366 {
5367 bitch(Rd, "Cannot delete account.", "Account doesn't exist.");
5368 ret++;
5369 }
5370 else
5371 {
5372// check if logged in user is allowed to delete this account
5373// delete user record
5374// log the user out if they are logged in
5375 }
5376 return ret;
5377}
5378
5379// The [create member] button on accountLoginWeb()
5380static int accountCreateSub(reqData *Rd, inputForm *iF, inputValue *iV)
5381{
5382 int ret = 0;
5383 char *uuid = Rd->shs.UUID, *first = getStrH(Rd->stuff, "name"), *last = NULL;
5384 int c = accountRead(Rd, uuid, first, last);
5385
5386 if (strcmp("POST", Rd->Method) == 0)
5387 {
5388 if (0 != c)
5389 {
5390 bitch(Rd, "Cannot create account.", "Account exists.");
5391 Rd->shs.status = SHS_NUKE;
5392 ret++;
5393 }
5394 else
5395 {
5396 char *salt = newSLOSsalt(Rd);
5397 char *h = checkSLOSpassword(Rd, salt, getStrH(Rd->body, "password"), NULL, NULL);
5398
5399 if (NULL == h)
5400 ret++;
5401 else
5402 {
5403 Rd->stuff->putstr(Rd->stuff, "passHash", h);
5404 Rd->stuff->putstr(Rd->stuff, "passSalt", salt);
5405 if (NULL != Rd->shs.name) free(Rd->shs.name);
5406 // So that we can get the name later when we show the account data entry page via GET.
5407 Rd->shs.name = Rd->stuff->getstr(Rd->stuff, "name", true);
5408 free(h);
5409 Rd->shs.status = SHS_REFRESH;
5410 }
5411 free(salt);
5412 if (0 != ret)
5413 Rd->shs.status = SHS_NUKE;
5414 }
5415 }
5416 return ret;
5417}
5418
5419// The [confirm] button on accountAddWeb()
5420static int accountAddSub(reqData *Rd, inputForm *iF, inputValue *iV)
5421{
5422 int ret = 0;
5423 char *uuid = Rd->shs.UUID, *first = getStrH(Rd->stuff, "firstName"), *last = getStrH(Rd->stuff, "lastName");
5424 int c = accountRead(Rd, uuid, first, last);
5425
5426 if (0 != c)
5427 {
5428 bitch(Rd, "Cannot add account.", "Account exists.");
5429 Rd->shs.status = SHS_NUKE;
5430 ret++;
5431 }
5432 else if ((0 == ret) && (strcmp("POST", Rd->Method) == 0))
5433 {
5434 char *h = checkSLOSpassword(Rd, getStrH(Rd->stuff, "passSalt"), getStrH(Rd->stuff, "password"), getStrH(Rd->stuff, "passHash"), "Passwords are not the same.");
5435
5436 if (NULL == h)
5437 {
5438 ret++;
5439 Rd->shs.status = SHS_NUKE;
5440 }
5441 else
5442 {
5443 free(h);
5444 generateAccountUUID(Rd);
5445 Rd->stuff->putstr(Rd->stuff, "passwordHash", getStrH(Rd->stuff, "passHash"));
5446 Rd->stuff->putstr(Rd->stuff, "passwordSalt", getStrH(Rd->stuff, "passSalt"));
5447 Rd->shs.level = -200;
5448 Rd->database->putstr(Rd->database, "UserAccounts.UserLevel", "-200");
5449 // Generate the linky for the email.
5450 newSesh(Rd, TRUE);
5451 accountWrite(Rd);
5452 // log them in
5453 I("Logged on %s %s Level %d %s", Rd->shs.UUID, Rd->shs.name, Rd->shs.level, getLevel(Rd->shs.level));
5454 Rd->output = "accountView";
5455 Rd->form = "accountView";
5456 Rd->doit = "login";
5457 Rd->shs.status = SHS_LOGIN;
5458 }
5459 }
5460 return ret;
5461}
5462
5463static int accountSaveSub(reqData *Rd, inputForm *iF, inputValue *iV)
5464{
5465 int ret = 0;
5466 // Using body[user] here, coz we got to this page via a URL query.
5467 char *uuid = Rd->shs.UUID, *first = getStrH(Rd->body, "user"), *last = NULL;
5468 int c = accountRead(Rd, NULL, first, last);
5469
5470 if (1 != c)
5471 {
5472 bitch(Rd, "Cannot save account.", "Account doesn't exist.");
5473 ret++;
5474 }
5475 else if ((0 == ret) && (strcmp("POST", Rd->Method) == 0))
5476 {
5477 Rd->stuff->putstr(Rd->stuff, "email", getStrH(Rd->database, "UserAccounts.Email"));
5478 Rd->stuff->putstr(Rd->stuff, "created", getStrH(Rd->database, "UserAccounts.Created"));
5479 Rd->stuff->putstr(Rd->stuff, "flags", getStrH(Rd->database, "UserAccounts.UserFlags"));
5480 Rd->stuff->putstr(Rd->stuff, "active", getStrH(Rd->database, "UserAccounts.active"));
5481 Rd->stuff->putstr(Rd->stuff, "passwordSalt", getStrH(Rd->database, "auth.passwordSalt"));
5482 Rd->stuff->putstr(Rd->stuff, "passwordHash", getStrH(Rd->database, "auth.passwordHash"));
5483 Rd->stuff->putstr(Rd->stuff, "name", getStrH(Rd->database, "Lua.name"));
5484 Rd->stuff->putstr(Rd->stuff, "DoB", getStrH(Rd->database, "Lua.DoB"));
5485 Rd->stuff->putstr(Rd->stuff, "agree", getStrH(Rd->database, "Lua.agree"));
5486 Rd->stuff->putstr(Rd->stuff, "adult", getStrH(Rd->database, "Lua.adult"));
5487 Rd->stuff->putstr(Rd->stuff, "aboutMe", getStrH(Rd->database, "Lua.aboutMe"));
5488 Rd->stuff->putstr(Rd->stuff, "vouched", getStrH(Rd->database, "Lua.vouched"));
5489 Rd->stuff->putstr(Rd->stuff, "voucher", getStrH(Rd->database, "Lua.voucher"));
5490
5491 char *lvl = getStrH(Rd->body, "level");
5492 qlisttbl_obj_t obj;
5493
5494 memset((void*)&obj, 0, sizeof(obj)); // must be cleared before call
5495 accountLevels->lock(accountLevels);
5496 while(accountLevels->getnext(accountLevels, &obj, NULL, false) == true)
5497 {
5498 if (strcmp(lvl, (char *) obj.data) == 0)
5499 Rd->database->putstr(Rd->database, "UserAccounts.UserLevel", obj.name);
5500 }
5501 accountLevels->unlock(accountLevels);
5502 accountWrite(Rd);
5503 free(Rd->outQuery);
5504 Rd->outQuery = xmprintf("?user=%s+%s", getStrH(Rd->database, "UserAccounts.FirstName"), getStrH(Rd->database, "UserAccounts.LastName"));
5505// TODO - this isn't being shown.
5506 addStrL(Rd->messages, "Account saved.");
5507 }
5508 return ret;
5509}
5510
5511// The unique validation URL sent in email.
5512static int accountValidateSub(reqData *Rd, inputForm *iF, inputValue *iV)
5513{
5514 int ret = 0;
5515 char *uuid = Rd->shs.UUID, *first = getStrH(Rd->stuff, "firstName"), *last = getStrH(Rd->stuff, "lastName");
5516 int c = accountRead(Rd, uuid, first, last);
5517
5518 if (1 != c)
5519 {
5520 bitch(Rd, "Cannot validate account.", "Account doesn't exist.");
5521 ret++;
5522 }
5523 else
5524 {
5525 Rd->stuff->putstr(Rd->stuff, "email", getStrH(Rd->database, "UserAccounts.Email"));
5526 Rd->stuff->putstr(Rd->stuff, "created", getStrH(Rd->database, "UserAccounts.Created"));
5527 Rd->stuff->putstr(Rd->stuff, "flags", getStrH(Rd->database, "UserAccounts.UserFlags"));
5528 Rd->stuff->putstr(Rd->stuff, "active", getStrH(Rd->database, "UserAccounts.active"));
5529 Rd->stuff->putstr(Rd->stuff, "passwordSalt", getStrH(Rd->database, "auth.passwordSalt"));
5530 Rd->stuff->putstr(Rd->stuff, "passwordHash", getStrH(Rd->database, "auth.passwordHash"));
5531 Rd->stuff->putstr(Rd->stuff, "name", getStrH(Rd->database, "Lua.name"));
5532 Rd->stuff->putstr(Rd->stuff, "DoB", getStrH(Rd->database, "Lua.DoB"));
5533 Rd->stuff->putstr(Rd->stuff, "agree", getStrH(Rd->database, "Lua.agree"));
5534 Rd->stuff->putstr(Rd->stuff, "adult", getStrH(Rd->database, "Lua.adult"));
5535 Rd->stuff->putstr(Rd->stuff, "aboutMe", getStrH(Rd->database, "Lua.aboutMe"));
5536 Rd->stuff->putstr(Rd->stuff, "vouched", getStrH(Rd->database, "Lua.vouched"));
5537 Rd->stuff->putstr(Rd->stuff, "voucher", getStrH(Rd->database, "Lua.voucher"));
5538 Rd->shs.level = -100;
5539 Rd->database->putstr(Rd->database, "UserAccounts.UserLevel", "-100");
5540 accountWrite(Rd);
5541 Rd->doit = "logout";
5542 Rd->output = "accountLogin";
5543 Rd->form = "accountLogin";
5544 Rd->shs.status = SHS_NUKE;
5545 }
5546 return ret;
5547}
5548
5549static int accountViewSub(reqData *Rd, inputForm *iF, inputValue *iV)
5550{
5551// TODO - this has to change when we are editing other peoples accounts.
5552 int ret = 0;
5553 char *uuid = Rd->shs.UUID, *first = getStrH(Rd->stuff, "firstName"), *last = getStrH(Rd->stuff, "lastName");
5554 int c = accountRead(Rd, uuid, first, last);
5555
5556d("Sub accountViewSub() %s %s %s", uuid, first, last);
5557 if (1 != c)
5558 {
5559 bitch(Rd, "Cannot view account.", "Account doesn't exist.");
5560 ret++;
5561 Rd->shs.status = SHS_NUKE;
5562 }
5563 else
5564 {
5565 // Check password on POST if the session user is the same as the shown user, coz this is the page shown on login.
5566 // Also only check on login.
5567 if ((strcmp("POST", Rd->Method) == 0) //&& (strcmp(Rd->shs.UUID, getStrH(Rd->database, "UserAccounts.PrincipalID")) == 0)
5568 && (strcmp("login", Rd->doit) == 0) && (strcmp("accountLogin", Rd->form) == 0))
5569 {
5570 char *h = checkSLOSpassword(Rd, getStrH(Rd->database, "auth.passwordSalt"), getStrH(Rd->body, "password"), getStrH(Rd->database, "auth.passwordHash"), "Login failed.");
5571 if (NULL == h)
5572 {
5573 ret++;
5574 Rd->shs.status = SHS_NUKE;
5575 }
5576 else
5577 {
5578 Rd->shs.level = atoi(getStrH(Rd->database, "UserAccounts.UserLevel"));
5579 if (NULL != Rd->shs.name) free(Rd->shs.name);
5580 Rd->shs.name = Rd->database->getstr(Rd->database, "Lua.name", true);
5581 if (NULL != Rd->shs.UUID) free(Rd->shs.UUID);
5582 Rd->shs.UUID = Rd->database->getstr(Rd->database, "UserAccounts.PrincipalID", true);
5583 free(h);
5584 I("Logged on %s %s Level %d %s", Rd->shs.UUID, Rd->shs.name, Rd->shs.level, getLevel(Rd->shs.level));
5585 Rd->shs.status = SHS_LOGIN;
5586 }
5587 }
5588 }
5589
5590 return ret;
5591}
5592static int accountEditSub(reqData *Rd, inputForm *iF, inputValue *iV)
5593{
5594 int ret = 0;
5595 char *uuid = Rd->shs.UUID, *first = getStrH(Rd->stuff, "firstName"), *last = getStrH(Rd->stuff, "lastName");
5596 int c = accountRead(Rd, uuid, first, last);
5597
5598d("Sub accountEditSub %s %s %s", uuid, first, last);
5599 if (1 != c)
5600 {
5601 bitch(Rd, "Cannot edit account.", "Account doesn't exist.");
5602 ret++;
5603 }
5604 else
5605 {
5606// check if logged in user is allowed to make these changes
5607// update user record
5608 }
5609 return ret;
5610}
5611
5612static int accountExploreSub(reqData *Rd, inputForm *iF, inputValue *iV)
5613{
5614 int ret = 0;
5615// get a list of user records
5616 return ret;
5617}
5618
5619static int accountOutSub(reqData *Rd, inputForm *iF, inputValue *iV)
5620{
5621 int ret = 0;
5622 char *uuid = Rd->shs.UUID, *first = getStrH(Rd->stuff, "firstName"), *last = getStrH(Rd->stuff, "lastName");
5623 int c = accountRead(Rd, uuid, first, last);
5624
5625 if (1 != c)
5626 {
5627// bitch(Rd, "Cannot logout account.", "Account doesn't exist.");
5628// ret++;
5629 }
5630
5631 Rd->shs.status = SHS_NUKE;
5632 return ret;
5633}
5634
5635/* TODO - instead of searching through all the users, ...
5636 have a bunch of separate folders with symlinks
5637 scData/users/aaproved
5638 scData/users/disabled
5639 scData/users/god
5640 onefang_rejected.lua -> ../uuid.lua
5641 scData/users/newbie
5642 foo_bar.lua -> ../uuid.lua
5643 scData/users/validated
5644
5645*/
5646typedef struct _RdAndListTbl RdAndListTbl;
5647struct _RdAndListTbl
5648{
5649 reqData *Rd;
5650 qlisttbl_t *list;
5651};
5652static int accountFilterValidated(struct dirtree *node)
5653{
5654 if (!node->parent) return DIRTREE_RECURSE | DIRTREE_SHUTUP;
5655
5656 if (S_ISREG(node->st.st_mode))
5657 {
5658 struct stat st;
5659 struct timespec now;
5660 RdAndListTbl *rdl = (RdAndListTbl *) node->parent->extra;
5661 qhashtbl_t *tnm = qhashtbl(0, 0);
5662 char *name = node->name;
5663 char *where = xmprintf("%s/users/%s", scData, node->name);
5664 int rt = LuaToHash(rdl->Rd, where, "user", tnm, 0, &st, &now, "user");
5665
5666t("accountFilterValidatedVoucher %s (%s) -> %s -> %s", name, getStrH(tnm, "level"), getStrH(tnm, "name"), getStrH(tnm, "voucher"));
5667 if ((0 == rt) && (strcmp("-100", getStrH(tnm, "level")) == 0))
5668 rdl->list->put(rdl->list, getStrH(tnm, "name"), &tnm, sizeof(qhashtbl_t *));
5669 else
5670 tnm->free(tnm);
5671 free(where);
5672 }
5673 return 0;
5674}
5675qlisttbl_t *getAccounts(reqData *Rd)
5676{
5677 qlisttbl_t *ret = qlisttbl(0);
5678 RdAndListTbl rdl = {Rd, ret};
5679 char *path = xmprintf("%s/users", scData);
5680 struct dirtree *new = dirtree_add_node(0, path, 0);
5681
5682 new->extra = (long) &rdl;
5683 dirtree_handle_callback(new, accountFilterValidated);
5684 ret->sort(ret);
5685 free(path);
5686
5687 return ret;
5688}
5689static void accountExploreValidatedVouchersWeb(reqData *Rd, inputForm *oF, inputValue *oV)
5690{
5691 qlisttbl_t *list =getAccounts(Rd);
5692
5693 if (NULL != Rd->shs.name) free(Rd->shs.name);
5694 Rd->shs.name = NULL;
5695 if (NULL != Rd->shs.UUID) free(Rd->shs.UUID);
5696 Rd->shs.UUID = NULL;
5697 Rd->shs.level = -256;
5698 accountWebHeaders(Rd, oF);
5699 accountWebFields(Rd, oF, oV);
5700
5701 count = list->size(list);
5702 Rd->reply->addstrf(Rd->reply, "<table border=\"1\"><caption>Validated users</caption>\n");
5703 Rd->reply->addstr(Rd->reply, "<tr>");
5704 Rd->reply->addstr(Rd->reply, "<th>name</th>");
5705 Rd->reply->addstr(Rd->reply, "<th>voucher</th>");
5706 Rd->reply->addstr(Rd->reply, "<th>level</th>");
5707 Rd->reply->addstr(Rd->reply, "<th>title</th>");
5708 Rd->reply->addstr(Rd->reply, "</tr>\n<tr>");
5709
5710 qlisttbl_obj_t obj;
5711 memset((void *) &obj, 0, sizeof(obj));
5712 list->lock(list);
5713 while(list->getnext(list, &obj, NULL, false) == true)
5714 {
5715 qhashtbl_t *tnm = *((qhashtbl_t **) obj.data);
5716 char *nm = qstrreplace("tr", xstrdup(obj.name), " ", "+");
5717
5718 Rd->reply->addstrf(Rd->reply, "<tr><td><a href='https://%s%s%s?user=%s'>%s</a></td>", Rd->Host, Rd->Script, Rd->Path, nm, obj.name);
5719 Rd->reply->addstrf(Rd->reply, "<td>%s</td><td>%s</td><td>%s</td></tr>", getStrH(tnm, "voucher"), getStrH(tnm, "level"), getStrH(tnm, "title"));
5720 free(nm);
5721 tnm->clear(tnm);
5722 list->removeobj(list, &obj);
5723 tnm->free(tnm);
5724 }
5725 list->unlock(list);
5726 Rd->reply->addstr(Rd->reply, "</table>");
5727 list->free(list);
5728
5729 accountWebSubs(Rd, oF);
5730 accountWebFooter(Rd, oF);
5731}
5732static int accountExploreValidatedVoucherSub(reqData *Rd, inputForm *iF, inputValue *iV)
5733{
5734 int ret = 0;
5735 return ret;
5736}
5737
5738
5739qhashtbl_t *accountPages = NULL;
5740inputForm *newInputForm(char *name, char *title, char *help, inputFormShowFunc web, inputFormShowFunc eWeb)
5741{
5742 inputForm *ret = xmalloc(sizeof(inputForm));
5743
5744d("newInputForm(%s)", name);
5745 ret->name = name; ret->title = title; ret->help = help;
5746 ret->web = web; ret->eWeb = eWeb;
5747 ret->fields = qlisttbl(QLISTTBL_THREADSAFE | QLISTTBL_UNIQUE | QLISTTBL_LOOKUPFORWARD);
5748 ret->subs = qhashtbl(0, 0);
5749 accountPages->put(accountPages, ret->name, ret, sizeof(inputForm));
5750 free(ret);
5751 return accountPages->get(accountPages, name, NULL, false);
5752}
5753
5754inputField *addInputField(inputForm *iF, signed char type, char *name, char *title, char *help, inputFieldValidFunc validate, inputFieldShowFunc web)
5755{
5756 inputField *ret = xzalloc(sizeof(inputField));
5757
5758//d("addInputField(%s, %s)", iF->name, name);
5759 ret->name = name; ret->title = title; ret->help = help;
5760 ret->validate = validate; ret->web = web; ret->type = type;
5761 ret->flags = FLD_EDITABLE;
5762 iF->fields->put(iF->fields, ret->name, ret, sizeof(inputField));
5763 free(ret);
5764 return iF->fields->get(iF->fields, name, NULL, false);
5765}
5766
5767void inputFieldExtra(inputField *ret, signed char flags, short viewLength, short maxLength)
5768{
5769 ret->flags = flags;
5770 ret->viewLength = viewLength; ret->maxLength = maxLength;
5771}
5772
5773void addSession(inputForm *iF)
5774{
5775 inputField *fld, **flds = xzalloc(3 * sizeof(*flds));
5776
5777//d("addSession(%s)", iF->name);
5778 flds[0] = addInputField(iF, LUA_TSTRING, "hashish", "hashish", "", sessionValidate, sessionWeb);
5779 inputFieldExtra(flds[0], FLD_HIDDEN, 0, 0);
5780 flds[1] = addInputField(iF, LUA_TSTRING, "toke_n_munchie", "toke_n_munchie", "", sessionValidate, sessionWeb);
5781 inputFieldExtra(flds[1], FLD_HIDDEN, 0, 0);
5782 fld = addInputField(iF, LUA_TGROUP, "sessionGroup", "sessionGroup", "", sessionValidate, sessionWeb);
5783 inputFieldExtra(fld, FLD_HIDDEN, 0, 0);
5784 fld->group = flds;
5785 flds[0]->group = flds;
5786 flds[1]->group = flds;
5787}
5788
5789void addEmailFields(inputForm *iF)
5790{
5791 inputField *fld, **flds = xzalloc(3 * sizeof(*flds));
5792
5793 flds[0] = addInputField(iF, LUA_TEMAIL, "email", "email", NULL, emailValidate, emailWeb);
5794 inputFieldExtra(flds[0], FLD_EDITABLE, 42, 254);
5795 flds[1] = addInputField(iF, LUA_TEMAIL, "emayl", "Re-enter your email, to be sure you got it correct",
5796 "A validation email will be sent to this email address, you will need to click on the link in it to continue your account creation.", emailValidate, emailWeb);
5797 inputFieldExtra(flds[1], FLD_EDITABLE, 42, 254);
5798 fld = addInputField(iF, LUA_TGROUP, "emailGroup", "emailGroup", "", emailValidate, NULL);
5799 inputFieldExtra(fld, FLD_HIDDEN, 0, 0);
5800 fld->group = flds;
5801 flds[0]->group = flds;
5802 flds[1]->group = flds;
5803}
5804
5805void addDoBFields(inputForm *iF)
5806{
5807 inputField *fld, **flds = xzalloc(3 * sizeof(*flds));
5808
5809 flds[0] = addInputField(iF, LUA_TSTRING, "DoByear", "year", NULL, DoBValidate, DoByWeb);
5810 flds[1] = addInputField(iF, LUA_TSTRING, "DoBmonth", "month", NULL, DoBValidate, DoBmWeb);
5811 fld = addInputField(iF, LUA_TGROUP, "DoBGroup", "DoBGroup", "", DoBValidate, DoBWeb);
5812 inputFieldExtra(fld, FLD_HIDDEN, 0, 0);
5813 fld->group = flds;
5814 flds[0]->group = flds;
5815 flds[1]->group = flds;
5816}
5817
5818void addLegalFields(inputForm *iF)
5819{
5820 inputField *fld, **flds = xzalloc(3 * sizeof(*flds));
5821
5822 flds[0] = addInputField(iF, LUA_TBOOLEAN, "adult", "I'm allegedly an adult in my country.", NULL, legalValidate, adultWeb);
5823 flds[1] = addInputField(iF, LUA_TBOOLEAN, "agree", "I accept the Terms of Service.", NULL, legalValidate, agreeWeb);
5824 fld = addInputField(iF, LUA_TGROUP, "legalGroup", "legalGroup", "", legalValidate, legalWeb);
5825 inputFieldExtra(fld, FLD_HIDDEN, 0, 0);
5826 fld->group = flds;
5827 flds[0]->group = flds;
5828 flds[1]->group = flds;
5829}
5830
5831inputSub *addSubmit(inputForm *iF, char *name, char *title, char *help, inputSubmitFunc submit, char *output)
5832{
5833 inputSub *ret = xmalloc(sizeof(inputSub));
5834
5835//d("addSubmit(%s, %s)", iF->name, name);
5836 ret->name = name; ret->title = title; ret->help = help; ret->submit = submit; ret->outputForm = output;
5837 iF->subs->put(iF->subs, ret->name, ret, sizeof(inputSub));
5838 free(ret);
5839 return iF->subs->get(iF->subs, name, NULL, false);
5840}
5841
5842
5843/* There should be some precedence for values overriding values here.
5844 https://www.w3.org/standards/webarch/protocols
5845 "This intro text is boilerplate for the beta release of w3.org." Fucking useless. Pffft
5846 https://www.w3.org/Protocols/
5847 Still nothing official, though the ENV / HEADER stuff tends to be about the protocol things, and cookies / body / queries are about the data things.
5848 http://docs.gantry.org/gantry4/advanced/setby
5849 Says that query overrides cookies, but that might be just for their platform.
5850 https://framework.zend.com/manual/1.11/en/zend.controller.request.html
5851 Says - "1. GET, 2. POST, 3. COOKIE, 4. SERVER, 5. ENV."
5852 We don't actually get the headers directly, it's all sent via the env.
5853
5854URL query Values actually provided by the user in the FORM, and other things.
5855POST body Values actually provided by the user in the FORM.
5856cookies
5857 https://stackoverflow.com/questions/4056306/how-to-handle-multiple-cookies-with-the-same-name
5858
5859headers includes HTTP_COOKIE and QUERY_STRING
5860env includes headers and HTTP_COOKIE and QUERY_STRING
5861
5862database Since all of the above are for updating the database anyway, this goes on the bottom, overridden by all.
5863 Though be wary of security stuff.
5864
5865 Sending cookie headers is a special case, multiples can be sent, otherwise headers are singletons, only send one for each name.
5866*/
5867char *sourceTypes[] =
5868{
5869 "cookies",
5870 "body",
5871 "queries",
5872 "stuff"
5873};
5874
5875static int collectFields(reqData *Rd, inputForm *iF, inputValue *iV, int t)
5876{
5877 int i = 0, j;
5878 qlisttbl_obj_t obj;
5879
5880 memset((void*)&obj, 0, sizeof(obj)); // must be cleared before call
5881 iF->fields->lock(iF->fields);
5882 while(iF->fields->getnext(iF->fields, &obj, NULL, false) == true)
5883 {
5884 inputField *fld = (inputField *) obj.data;
5885
5886//if (0 > t)
5887// d("Collecting %d %s - %s", t, iF->name, fld->name);
5888//else
5889// d("Collecting %s %s - %s", sourceTypes[t], iF->name, fld->name);
5890 iV[i].field = fld;
5891 if (LUA_TGROUP == fld->type)
5892 {
5893 if (0 >= t)
5894 {
5895 j = 0;
5896 // If it's a group, number the members relative to the group field.
5897 // Assume the members for this group are the previous ones.
5898 while (iV[i].field->group[j])
5899 {
5900 j++;
5901 iV[i - j].index = j;
5902 }
5903 }
5904 }
5905 else
5906 {
5907 char *vl = NULL;
5908
5909 switch (t)
5910 {
5911 // We don't get the cookies metadata.
5912 case 0 : vl = Rd->cookies->getstr(Rd->cookies, obj.name, false); break;
5913 case 1 : vl = Rd->body-> getstr(Rd->body, obj.name, false); break;
5914 case 2 : vl = Rd->queries->getstr(Rd->queries, obj.name, false); break;
5915 case 3 : vl = Rd->queries->getstr(Rd->stuff, obj.name, false); break;
5916 default: break;
5917 }
5918 if ((NULL != iV[i].value) && (NULL != vl))
5919 {
5920 if (strcmp(vl, iV[i].value) != 0)
5921 W("Collected %s value for %s - %s from %s overriding value from %s", sourceTypes[t], iF->name, fld->name, sourceTypes[t], sourceTypes[iV[i].source]);
5922 else
5923 W("Collected %s value for %s - %s from %s same as value from %s", sourceTypes[t], iF->name, fld->name, sourceTypes[t], sourceTypes[iV[i].source]);
5924 }
5925 if (NULL != vl)
5926 {
5927 iV[i].source = t;
5928 iV[i].value = vl;
5929 D("Collected %s value for %s - %s = %s", sourceTypes[t], iF->name, fld->name, vl);
5930 }
5931 }
5932 i++;
5933 }
5934 iF->fields->unlock(iF->fields);
5935 return i;
5936}
5937
5938
5939void sessionStateEngine(reqData *Rd, char *type)
5940{
5941 switch (Rd->shs.status)
5942 {
5943 case SHS_UNKNOWN: d("sessionStateEngine(SHS_UNKNOWN, %s)", type); break;
5944 case SHS_NONE: d("sessionStateEngine(SHS_NONE, %s)", type); break;
5945 case SHS_BOGUS: d("sessionStateEngine(SHS_BOGUS, %s)", type); break;
5946 case SHS_PROBLEM: d("sessionStateEngine(SHS_PROBLEM, %s)", type); break;
5947 case SHS_VALID: d("sessionStateEngine(SHS_VALID, %s)", type); break;
5948
5949 case SHS_LOGIN: d("sessionStateEngine(SHS_LOGIN, %s)", type); break;
5950
5951 case SHS_RENEW: d("sessionStateEngine(SHS_RENEW, %s)", type); break;
5952 case SHS_REFRESH: d("sessionStateEngine(SHS_REFRESH, %s)", type); break;
5953 case SHS_IDLE: d("sessionStateEngine(SHS_IDLE, %s)", type); break;
5954 case SHS_ANCIENT: d("sessionStateEngine(SHS_ANCIENT, %s)", type); break;
5955
5956 case SHS_SECURITY: d("sessionStateEngine(SHS_SECURITY, %s)", type); break;
5957 case SHS_RELOGIN: d("sessionStateEngine(SHS_RELOGIN, %s)", type); break;
5958
5959 case SHS_KEEP: d("sessionStateEngine(SHS_KEEP, %s)", type); break;
5960 case SHS_WIPE: d("sessionStateEngine(SHS_WIPE, %s)", type); break;
5961 case SHS_NUKE: d("sessionStateEngine(SHS_NUKE, %s)", type); break;
5962 }
5963}
5964
5965
5966void account_html(char *file, reqData *Rd, HTMLfile *thisFile)
5967{
5968 inputForm *iF;
5969 inputField *fld;
5970 inputSub *sub;
5971 boolean isGET = FALSE;
5972 int e = 0, t = 0, i, j;
5973 char *doit = getStrH(Rd->body, "doit"), *form = getStrH(Rd->body, "form");
5974
5975 if (NULL == accountLevels)
5976 {
5977 accountLevels = qlisttbl(QLISTTBL_LOOKUPFORWARD | QLISTTBL_THREADSAFE | QLISTTBL_UNIQUE);
5978 accountLevels->putstr(accountLevels, "-256", "disabled");
5979 accountLevels->putstr(accountLevels, "-200", "newbie");
5980 accountLevels->putstr(accountLevels, "-100", "validated");
5981 accountLevels->putstr(accountLevels, "-50", "vouched for");
5982 accountLevels->putstr(accountLevels, "0", "approved"); // Note that http://opensimulator.org/wiki/Userlevel claims that 1 and above are "GOD_LIKE".
5983 accountLevels->putstr(accountLevels, "200", "god");
5984 }
5985
5986 // Check "Origin" header and /or HTTP_REFERER header.
5987 // "Origin" is either HTTP_HOST or X-FORWARDED-HOST. Which could be "null".
5988 char *ref = xmprintf("https://%s%s/account.html", getStrH(Rd->headers, "SERVER_NAME"), getStrH(Rd->headers, "SCRIPT_NAME"));
5989 char *href = Rd->headers->getstr(Rd->headers, "HTTP_REFERER", true);
5990
5991 if (NULL != href)
5992 {
5993 char *f = strchr(href, '?');
5994
5995 if (NULL != f)
5996 *f = '\0';
5997 if (('\0' != href[0]) && (strcmp(ref, href) != 0))
5998 {
5999 bitch(Rd, "Invalid referer.", ref);
6000 D("Invalid referer - %s isn't %s", ref, href);
6001 form = "accountLogin";
6002 Rd->shs.status = SHS_PROBLEM;
6003 }
6004 free(href);
6005 }
6006 free(ref);
6007 ref = getStrH(Rd->headers, "SERVER_NAME");
6008 href = getStrH(Rd->headers, "HTTP_HOST");
6009 if ('\0' == href[0])
6010 href = getStrH(Rd->headers, "X-FORWARDED-HOST");
6011 if (('\0' != href[0]) && (strcmp(ref, href) != 0))
6012 {
6013 bitch(Rd, "Invalid HOST.", ref);
6014 D("Invalid HOST - %s isn't %s", ref, href);
6015 form = "accountLogin";
6016 Rd->shs.status = SHS_PROBLEM;
6017 }
6018
6019 // Redirect to HTTPS if it's HTTP.
6020 if (strcmp("https", Rd->Scheme) != 0)
6021 {
6022 Rd->Rheaders->putstr (Rd->Rheaders, "Status", "301 Moved Permanently");
6023 Rd->Rheaders->putstrf(Rd->Rheaders, "Location", "https://%s%s", Rd->Host, Rd->RUri);
6024 Rd->reply->addstrf(Rd->reply, "<html><title>404 Unknown page</title><head>"
6025 "<meta http-equiv='refresh' content='0; URL=https://%s%s' />"
6026 "</head><body>You should get redirected to <a href='https://%s%s'>https://%s%s</a></body></html>",
6027 Rd->Host, Rd->RUri, Rd->Host, Rd->RUri, Rd->Host, Rd->RUri
6028 );
6029 D("Redirecting dynamic page %s -> https://%s%s", file, Rd->Host, Rd->RUri);
6030 return;
6031 }
6032
6033 // Create the dynamic web pages for account.html.
6034 if (NULL == accountPages)
6035 {
6036 accountPages = qhashtbl(0, 0);
6037
6038
6039 iF = newInputForm("accountAdd", "", NULL, accountAddWeb, accountLoginWeb);
6040 addSession(iF);
6041// fld = addInputField(iF, LUA_TSTRING, "UUID", "UUID", NULL, UUIDValidate, UUIDWeb);
6042// inputFieldExtra(fld, FLD_HIDDEN, 0, 0);
6043 fld = addInputField(iF, LUA_TSTRING, "name", "name", NULL, nameValidate, nameWeb);
6044 inputFieldExtra(fld, FLD_EDITABLE | FLD_REQUIRED, 42, 63);
6045 fld = addInputField(iF, LUA_TPASSWORD, "psswrd", "Re-enter your password",
6046 "Warning, the limit on password length is set by your viewer, some can't handle longer than 16 characters.", passwordValidate, passwordWeb);
6047 inputFieldExtra(fld, FLD_EDITABLE, 16, 0);
6048 addEmailFields(iF);
6049 addDoBFields(iF);
6050 addLegalFields(iF);
6051 fld = addInputField(iF, LUA_TSTRING, "ToS", "Terms of Service", "", NULL, ToSWeb);
6052 fld = addInputField(iF, LUA_TSTRING, "voucher", "The grid name of someone that will vouch for you",
6053 "We use a vouching system here, an existing member must know you well enough to tell us you'll be good for our grid.", voucherValidate, voucherWeb);
6054 inputFieldExtra(fld, FLD_EDITABLE, 42, 63);
6055 fld = addInputField(iF, LUA_TSTRING, "aboutMe", "About me", NULL, aboutMeValidate, aboutMeWeb);
6056 inputFieldExtra(fld, FLD_EDITABLE, 50, 16384);
6057 addSubmit(iF, "confirm", "confirm", NULL, accountAddSub, "accountView");
6058 addSubmit(iF, "cancel", "cancel", NULL, accountOutSub, "accountLogin");
6059
6060
6061 iF = newInputForm("accountView", "account view", NULL, accountViewWeb, accountLoginWeb);
6062 addSession(iF);
6063// fld = addInputField(iF, LUA_TSTRING, "UUID", "UUID", NULL, UUIDValidate, UUIDWeb);
6064// inputFieldExtra(fld, FLD_HIDDEN, 0, 0);
6065 fld = addInputField(iF, LUA_TSTRING, "name", "name", NULL, nameValidate, nameWeb);
6066 inputFieldExtra(fld, FLD_HIDDEN, 42, 63);
6067 fld = addInputField(iF, LUA_TSTRING, "user", "user", NULL, nameValidate, nameWeb);
6068 inputFieldExtra(fld, FLD_HIDDEN, 42, 63);
6069 addSubmit(iF, "login", "", NULL, accountViewSub, "accountView"); // Coz we sometimes want to trigger this from code.
6070 addSubmit(iF, "validate", "", NULL, accountValidateSub, "accountLogin"); // Coz we sometimes want to trigger this from code.
6071 addSubmit(iF, "edit", "", NULL, accountEditSub, "accountEdit"); // Coz we sometimes want to trigger this from code.
6072 addSubmit(iF, "validated_members", "validated members", NULL, accountExploreValidatedVoucherSub, "accountValidated");
6073 addSubmit(iF, "logout", "logout", NULL, accountOutSub, "accountLogin");
6074
6075
6076 iF = newInputForm("accountValidated", "account validated list", NULL, accountExploreValidatedVouchersWeb, accountLoginWeb);
6077 addSession(iF);
6078 fld = addInputField(iF, LUA_TSTRING, "name", "name", NULL, nameValidate, nameWeb);
6079 inputFieldExtra(fld, FLD_HIDDEN, 42, 63);
6080 addSubmit(iF, "login", "", NULL, accountViewSub, "accountView"); // Coz we sometimes want to trigger this from code.
6081 addSubmit(iF, "back", "back", NULL, accountViewSub, "accountView");
6082
6083
6084 iF = newInputForm("accountEdit", "account edit", NULL, accountEditWeb, accountLoginWeb);
6085 addSession(iF);
6086// fld = addInputField(iF, LUA_TSTRING, "UUID", "UUID", NULL, UUIDValidate, UUIDWeb);
6087// inputFieldExtra(fld, FLD_HIDDEN, 0, 0);
6088// fld = addInputField(iF, LUA_TSTRING, "name", "name", NULL, nameValidate, nameWeb);
6089// inputFieldExtra(fld, FLD_HIDDEN, 42, 63);
6090// fld = addInputField(iF, LUA_TSTRING, "user", "user", NULL, nameValidate, nameWeb);
6091// inputFieldExtra(fld, FLD_HIDDEN, 42, 63);
6092// fld = addInputField(iF, LUA_TEMAIL, "email", "email", "", emailValidate, emailWeb);
6093// inputFieldExtra(fld, FLD_NONE, 42, 254);
6094 addSubmit(iF, "login", "", NULL, accountViewSub, "accountView"); // Coz we sometimes want to trigger this from code.
6095 addSubmit(iF, "save", "save", NULL, accountSaveSub, "accountView");
6096 addSubmit(iF, "back", "back", NULL, accountViewSub, "accountView");
6097// addSubmit(iF, "members", "members", NULL, accountExploreSub, "accountExplore");
6098 addSubmit(iF, "logout", "logout", NULL, accountOutSub, "accountLogin");
6099// addSubmit(iF, "delete", "delete", NULL, accountDelSub, "accountDel");
6100
6101
6102 iF = newInputForm("accountLogin", "account login", "Please login, or create your new account.", accountLoginWeb, accountLoginWeb);
6103 addSession(iF);
6104 fld = addInputField(iF, LUA_TSTRING, "name", "name", "Your name needs to be two words, only ordinary letters and digits, no special characters or fonts.", nameValidate, nameWeb);
6105 inputFieldExtra(fld, FLD_EDITABLE | FLD_REQUIRED, 42, 63);
6106 fld = addInputField(iF, LUA_TPASSWORD, "password", "password",
6107 "Warning, the limit on password length is set by your viewer, some can't handle longer than 16 characters.", passwordValidate, passwordWeb);
6108 inputFieldExtra(fld, FLD_EDITABLE | FLD_REQUIRED, 16, 0);
6109 addSubmit(iF, "logout", "", NULL, accountOutSub, "accountLogin"); // Coz we sometimes want to trigger this from code.
6110 addSubmit(iF, "validate", "", NULL, accountValidateSub, "accountLogin"); // Coz we sometimes want to trigger this from code.
6111 addSubmit(iF, "login", "login", NULL, accountViewSub, "accountView");
6112 addSubmit(iF, "create", "create account", NULL, accountCreateSub, "accountAdd");
6113 }
6114
6115 // Figure out what we are doing.
6116 if ('\0' == form[0])
6117 form = getStrH(Rd->cookies, "form");
6118 if ('\0' == form[0])
6119 {
6120 form = "accountLogin";
6121 Rd->shs.status = SHS_NUKE;
6122 }
6123 if ('\0' == doit[0])
6124 doit = getStrH(Rd->cookies, "doit");
6125 if ('\0' == doit[0])
6126 {
6127 doit = "logout";
6128 Rd->shs.status = SHS_NUKE;
6129 }
6130 if ('\0' != doit[0])
6131 {
6132 setCookie(Rd, "doit", doit);
6133 Rd->doit = doit;
6134 }
6135
6136 iF = accountPages->get(accountPages, form, NULL, false);
6137 if (NULL == iF)
6138 {
6139 E("No such account page - %s", form);
6140 form = "accountLogin";
6141 doit = "logout";
6142 Rd->shs.status = SHS_PROBLEM;
6143 iF = accountPages->get(accountPages, form, NULL, false);
6144 }
6145 sub = iF->subs->get(iF->subs, doit, NULL, false);
6146 if (NULL == sub)
6147 {
6148 E("No such account action - %s", doit);
6149 form = "accountLogin";
6150 doit = "logout";
6151 Rd->shs.status = SHS_PROBLEM;
6152 iF = accountPages->get(accountPages, form, NULL, false);
6153 sub = iF->subs->get(iF->subs, doit, NULL, false);
6154 }
6155
6156 // Special for showing a users details.
6157 if ('\0' != getStrH(Rd->queries, "user")[0])
6158 {
6159 doit = "edit";
6160 form = "accountView";
6161 iF = accountPages->get(accountPages, form, NULL, false);
6162 sub = iF->subs->get(iF->subs, doit, NULL, false);
6163 }
6164
6165 Rd->doit = doit;
6166 Rd->form = form;
6167 Rd->output = sub->outputForm;
6168
6169 C("Starting dynamic page %s %s -> %s [%s -> %s]", Rd->RUri, form, doit, sub->name, Rd->output);
6170
6171 // Collect the input data.
6172 int count = iF->fields->size(iF->fields);
6173 inputValue *iV = xzalloc(count * sizeof(inputValue));
6174 qlisttbl_obj_t obj;
6175
6176 sessionStateEngine(Rd, "collected");
6177
6178 if (strcmp("cancel", sub->name) != 0)
6179 {
6180
6181// TODO - complain about any extra body or query parameter that we where not expecting.
6182 for (t = 0; t < 3; t++)
6183 i = collectFields(Rd, iF, iV, t);
6184
6185 // Validate the input data.
6186 D("For page %s -> %s, start of validation.", iF->name, sub->name);
6187 t = i;
6188 for (i = 0; i < t; i++)
6189 {
6190 if ((NULL != iV[i].value) || (LUA_TGROUP == iV[i].field->type))
6191 {
6192 if (NULL == iV[i].field->validate)
6193 E("No validation function for %s - %s", iF->name, iV[i].field->name);
6194 else
6195 {
6196 if (0 == iV[i].valid)
6197 {
6198 D("Validating %s - %s", iF->name, iV[i].field->name);
6199 int rt = iV[i].field->validate(Rd, iF, &iV[i]);
6200 if (rt)
6201 {
6202 W("Invalidated %s - %s returned %d errors", iF->name, iV[i].field->name, rt);
6203 iV[i].valid = -1;
6204 }
6205 else
6206 {
6207 D("Validated %s - %s", iF->name, iV[i].field->name);
6208 iV[i].valid = 1;
6209 }
6210 e += rt;
6211 if (NULL != iV[i].field->group)
6212 {
6213 // Use the indexes to set the validation result for the other members of the group.
6214 // The assumption is that the validation functions for each are the same, and it validates the members as a group.
6215 for (j = iV[i].index; j > 0; j--)
6216 iV[i + j].valid = iV[i].valid;
6217// TODO - Possible off by one error, but I don't think it matters. Needs more testing.
6218 for (j = 0; j <= iV[i].index; j++)
6219 iV[i + j].valid = iV[i].valid;
6220 }
6221 }
6222 else if (0 > iV[i].valid)
6223 D("Already invalidated %s - %s", iF->name, iV[i].field->name);
6224 else if (0 < iV[i].valid)
6225 D("Already validated %s - %s", iF->name, iV[i].field->name);
6226 }
6227 }
6228 doit = Rd->doit;
6229 form = Rd->form;
6230 }
6231
6232 sessionStateEngine(Rd, "validated");
6233
6234 // Submit the data. Reload input form and sub in case things got changed by the validation functions.
6235 iF = accountPages->get(accountPages, Rd->form, NULL, false);
6236 sub = iF->subs->get(iF->subs, Rd->doit, NULL, false);
6237//if (strcmp("POST", Rd->Method) == 0) // Not such a good idea, since we are faking a submit for account validation, which is a GET.
6238{
6239 if (0 == e)
6240 {
6241 D("For page %s, start of %s submission.", iF->name, sub->name);
6242 if (NULL != sub->submit)
6243 e += sub->submit(Rd, iF, iV);
6244 }
6245}
6246 free(iV);
6247
6248 }
6249 else
6250 {
6251 sessionStateEngine(Rd, "CANCELLED");
6252 }
6253
6254 sessionStateEngine(Rd, "submited");
6255 switch (Rd->shs.status)
6256 {
6257 case SHS_RENEW:
6258 case SHS_REFRESH:
6259 {
6260 freeSesh(Rd, FALSE, FALSE);
6261 newSesh(Rd, FALSE);
6262 break;
6263 }
6264
6265 case SHS_VALID:
6266 case SHS_KEEP:
6267 {
6268 // Do nothing here.
6269 break;
6270 }
6271
6272 case SHS_LOGIN:
6273 case SHS_WIPE:
6274 {
6275// TODO - should wipe the old one, and create this new one with the users UUID.
6276// I think that's what we are doing anyway.
6277 freeSesh(Rd, FALSE, FALSE);
6278 newSesh(Rd, FALSE);
6279 break;
6280 }
6281
6282// TODO - these three should store state, so they can go back to where the user was (or where they where going) before asking them to confirm their login credentials.
6283 case SHS_IDLE:
6284 case SHS_SECURITY:
6285 case SHS_RELOGIN:
6286
6287 case SHS_UNKNOWN:
6288 case SHS_NONE:
6289 case SHS_BOGUS:
6290 case SHS_PROBLEM:
6291 case SHS_ANCIENT:
6292 case SHS_NUKE:
6293 {
6294 freeSesh(Rd, FALSE, TRUE); // Wipe mode clears out all of Rd->database, selected Rd->stuff, and the above commented out Rd->shs.
6295 newSesh(Rd, FALSE);
6296 form = "accountLogin";
6297 doit = "logout";
6298 Rd->doit = doit;
6299 Rd->form = form;
6300 iF = accountPages->get(accountPages, Rd->form, NULL, false);
6301 sub = iF->subs->get(iF->subs, Rd->doit, NULL, false);
6302 Rd->output = sub->outputForm;
6303 break;
6304 }
6305 }
6306
6307 // Return the result.
6308 if (0 == e)
6309 {
6310 if (strcmp("GET", Rd->Method) == 0)
6311 {
6312 // Find the output form.
6313 inputForm *oF = accountPages->get(accountPages, Rd->output, NULL, false);
6314 if (NULL == oF)
6315 {
6316 E("No such account page - %s", Rd->output);
6317 form = "accountLogin";
6318 doit = "logout";
6319 oF = accountPages->get(accountPages, form, NULL, false);
6320 }
6321 D("Building output page %s", oF->name);
6322 count = oF->fields->size(oF->fields);
6323 iV = xzalloc(count * sizeof(inputValue));
6324 collectFields(Rd, oF, iV, -1);
6325 collectFields(Rd, oF, iV, 3);
6326 oF->web(Rd, oF, iV);
6327 free(iV);
6328 }
6329 else
6330 {
6331 // Redirect to a GET if it was a POST.
6332 // The reason here is to avoid a refresh turning into a rePOST.
6333 if ((strcmp("POST", Rd->Method) == 0))
6334 {
6335 if ('\0' != Rd->form[0])
6336 setCookie(Rd, "form", Rd->form);
6337 if ('\0' != Rd->doit[0])
6338 setCookie(Rd, "doit", Rd->doit);
6339 Rd->Rheaders->putstr (Rd->Rheaders, "Status", "303 See Other");
6340 Rd->Rheaders->putstrf(Rd->Rheaders, "Location", "https://%s%s%s", Rd->Host, Rd->RUri, Rd->outQuery);
6341 Rd->reply->addstrf(Rd->reply, "<html><title>Post POST redirect</title><head>"
6342 "<meta http-equiv='refresh' content='0; URL=https://%s%s%s' />"
6343 "</head><body>You should get redirected to <a href='https://%s%s%s'>https://%s%s%s</a></body></html>",
6344 Rd->Host, Rd->RUri, Rd->outQuery, Rd->Host, Rd->RUri, Rd->outQuery, Rd->Host, Rd->RUri, Rd->outQuery
6345 );
6346 I("Redirecting dynamic page %s -> https://%s%s%s (%s)", file, Rd->Host, Rd->RUri, Rd->outQuery, Rd->form);
6347 }
6348 }
6349 }
6350 else
6351 {
6352 if (0 < e)
6353 E("Building output ERROR page %s, coz of %d errors.", iF->name, e);
6354 else
6355 D("Building alternate output page %s", iF->name);
6356 // First just sort out input groups, then get the data from Rd->stuff.
6357 count = iF->fields->size(iF->fields);
6358 iV = xzalloc(count * sizeof(inputValue));
6359 collectFields(Rd, iF, iV, -1);
6360 collectFields(Rd, iF, iV, 3);
6361 iF->eWeb(Rd, iF, iV);
6362 free(iV);
6363 }
6364
6365 free(Rd->outQuery);
6366 Rd->outQuery = NULL;
6367
6368 C("Ending dynamic page %s %s", Rd->RUri, form);
6369}
6370
6371
6372static void cleanup(void)
6373{
6374// TODO - not sure why, but this gets called twice on quitting sometimes.
6375 C("Caught signal, or quitting, cleaning up.");
6376
6377 if (accountPages)
6378 {
6379 qhashtbl_obj_t obj;
6380
6381 memset((void*)&obj, 0, sizeof(obj));
6382 accountPages->lock(accountPages);
6383 while(accountPages->getnext(accountPages, &obj, false) == true)
6384 {
6385 inputForm *f = (inputForm *) obj.data;
6386
6387 f->subs->free(f->subs);
6388 qlisttbl_obj_t fobj;
6389
6390 memset((void *) &fobj, 0, sizeof(fobj));
6391 f->fields->lock(f->fields);
6392 while(f->fields->getnext(f->fields, &fobj, NULL, false) == true)
6393 {
6394 inputField *fld = (inputField *) fobj.data;
6395
6396 if (LUA_TGROUP == fld->type)
6397 free(fld->group);
6398 }
6399 f->fields->unlock(f->fields);
6400 f->fields->free(f->fields);
6401 }
6402 accountPages->unlock(accountPages);
6403 accountPages->free(accountPages);
6404 accountPages = NULL;
6405 }
6406
6407 if (dynPages) dynPages->free(dynPages);
6408 dynPages = NULL;
6409 if (HTMLfileCache)
6410 {
6411 qhashtbl_obj_t obj;
6412
6413 memset((void*)&obj, 0, sizeof(obj));
6414 HTMLfileCache->lock(HTMLfileCache);
6415 while(HTMLfileCache->getnext(HTMLfileCache, &obj, false) == true)
6416 {
6417 HTMLfile *f = (HTMLfile *) obj.data;
6418
6419 unfragize(f->fragments, NULL, TRUE);
6420 }
6421 HTMLfileCache->unlock(HTMLfileCache);
6422 HTMLfileCache->free(HTMLfileCache);
6423 HTMLfileCache = NULL;
6424 }
6425 if (mimeTypes) mimeTypes->free(mimeTypes);
6426 mimeTypes = NULL;
6427 freeDb(true);
6428 if (L) lua_close(L);
6429 L = NULL;
6430 if (stats)
6431 {
6432 if (stats->stats) stats->stats->free(stats->stats);
6433 free(stats);
6434 stats = NULL;
6435 }
6436 if (configs) configs->free(configs);
6437 configs = NULL;
6438}
6439
6440
6441void sledjchisl_main(void)
6442{
6443 char *cmd = *toys.optargs;
6444 char *tmp;
6445 struct stat statbuf;
6446 int status, result, i;
6447 void *vd;
6448
6449 configs = qhashtbl(0, 0);
6450 L = luaL_newstate();
6451
6452 I("libfcgi version: %s", FCGI_VERSION);
6453 I("Lua version: %s", LUA_RELEASE);
6454 I("LuaJIT version: %s", LUAJIT_VERSION);
6455 I("MariaDB / MySQL client version: %s", mysql_get_client_info());
6456 I("OpenSSL version: %s", OPENSSL_VERSION_TEXT);
6457 I("qLibc version: qLibc only git tags for version numbers. Sooo, 2.4.4, unless I forgot to update this.");
6458 I("toybox version: %s", TOYBOX_VERSION);
6459
6460 dbRequests = qlist(0);
6461 sigatexit(cleanup);
6462
6463 pwd = getcwd(0, 0);
6464
6465 if (-1 == fstat(STDIN_FILENO, &statbuf))
6466 {
6467 error_msg("fstat() failed");
6468 if (1 != isatty(STDIN_FILENO))
6469 isWeb = TRUE;
6470 }
6471 else
6472 {
6473 if (S_ISREG (statbuf.st_mode)) D("STDIN is a regular file.");
6474 else if (S_ISDIR (statbuf.st_mode)) D("STDIN is a directory.");
6475 else if (S_ISCHR (statbuf.st_mode)) D("STDIN is a character device.");
6476 else if (S_ISBLK (statbuf.st_mode)) D("STDIN is a block device.");
6477 else if (S_ISFIFO(statbuf.st_mode)) D("STDIN is a FIFO (named pipe).");
6478 else if (S_ISLNK (statbuf.st_mode)) D("STDIN is a symbolic link.");
6479 else if (S_ISSOCK(statbuf.st_mode)) D("STDIN is a socket.");
6480 else D("STDIN is a unknown file descriptor type.");
6481 if (!S_ISCHR(statbuf.st_mode))
6482 isWeb = TRUE;
6483 }
6484
6485 // Print our user name and groups.
6486 struct passwd *pw;
6487 struct group *grp;
6488 uid_t euid = geteuid();
6489 gid_t egid = getegid();
6490 gid_t *groups = (gid_t *)toybuf;
6491 i = sizeof(toybuf)/sizeof(gid_t);
6492 int ngroups;
6493
6494 pw = xgetpwuid(euid);
6495 I("Running as user %s", pw->pw_name);
6496
6497 grp = xgetgrgid(egid);
6498 ngroups = getgroups(i, groups);
6499 if (ngroups < 0) perror_exit("getgroups");
6500 D("User is in group %s", grp->gr_name);
6501 for (i = 0; i < ngroups; i++) {
6502 if (groups[i] != egid)
6503 {
6504 if ((grp = getgrgid(groups[i])))
6505 D("User is in group %s", grp->gr_name);
6506 else
6507 D("User is in group %u", groups[i]);
6508 }
6509 }
6510
6511
6512/* From http://luajit.org/install.html -
6513To change or extend the list of standard libraries to load, copy
6514src/lib_init.c to your project and modify it accordingly. Make sure the
6515jit library is loaded or the JIT compiler will not be activated.
6516*/
6517 luaL_openlibs(L); // Load Lua libraries.
6518
6519 // Load the config scripts.
6520 char *cPaths[] =
6521 {
6522 ".sledjChisl.conf.lua",
6523 "/etc/sledjChisl.conf.lua",
6524// "/etc/sledjChisl.d/*.lua",
6525 "~/.sledjChisl.conf.lua",
6526// "~/.config/sledjChisl/*.lua",
6527 NULL
6528 };
6529 struct stat st;
6530
6531
6532 for (i = 0; cPaths[i]; i++)
6533 {
6534 memset(toybuf, 0, sizeof(toybuf));
6535 if (('/' == cPaths[i][0]) || ('~' == cPaths[i][0]))
6536 snprintf(toybuf, sizeof(toybuf), "%s", cPaths[i]);
6537 else
6538 snprintf(toybuf, sizeof(toybuf), "%s/%s", pwd, cPaths[i]);
6539 if (0 != lstat(toybuf, &st))
6540 continue;
6541 I("Loading configuration file - %s", toybuf);
6542 status = luaL_loadfile(L, toybuf);
6543 if (status) // If something went wrong, error message is at the top of the stack.
6544 E("Couldn't load file: %s", lua_tostring(L, -1));
6545 else
6546 {
6547 result = lua_pcall(L, 0, LUA_MULTRET, 0);
6548 if (result)
6549 E("Failed to run script: %s", lua_tostring(L, -1));
6550 else
6551 {
6552 lua_getglobal(L, "config");
6553 lua_pushnil(L);
6554 while(lua_next(L, -2) != 0)
6555 {
6556 char *n = (char *) lua_tostring(L, -2);
6557
6558 // Numbers can convert to strings, so check for numbers before checking for strings.
6559 // On the other hand, strings that can be converted to numbers also pass lua_isnumber(). sigh
6560 if (lua_isnumber(L, -1))
6561 {
6562 float v = lua_tonumber(L, -1);
6563 configs->put(configs, n, &v, sizeof(float));
6564 }
6565 else if (lua_isstring(L, -1))
6566 configs->putstr(configs, n, (char *) lua_tostring(L, -1));
6567 else if (lua_isboolean(L, -1))
6568 {
6569 int v = lua_toboolean(L, -1);
6570 configs->putint(configs, n, v);
6571 }
6572 else
6573 {
6574 char *v = (char *) lua_tostring(L, -1);
6575 E("Unknown config variable type for %s = %s", n, v);
6576 }
6577 lua_pop(L, 1);
6578 }
6579 }
6580 }
6581 }
6582 DEBUG = configs->getint(configs, "debug");
6583 D("Setting DEBUG = %d", DEBUG);
6584 if ((vd = configs->get (configs, "loadAverageInc", NULL, false)) != NULL) {loadAverageInc = *((float *) vd); D("Setting loadAverageInc = %f", loadAverageInc);}
6585 if ((vd = configs->get (configs, "simTimeOut", NULL, false)) != NULL) {simTimeOut = (int) *((float *) vd); D("Setting simTimeOut = %d", simTimeOut);}
6586 if ((tmp = configs->getstr(configs, "scRoot", false)) != NULL) {scRoot = tmp; D("Setting scRoot = %s", scRoot);}
6587 if ((tmp = configs->getstr(configs, "scUser", false)) != NULL) {scUser = tmp; D("Setting scUser = %s", scUser);}
6588 if ((tmp = configs->getstr(configs, "Tconsole", false)) != NULL) {Tconsole = tmp; D("Setting Tconsole = %s", Tconsole);}
6589 if ((tmp = configs->getstr(configs, "Tsocket", false)) != NULL) {Tsocket = tmp; D("Setting Tsocket = %s", Tsocket);}
6590 if ((tmp = configs->getstr(configs, "Ttab", false)) != NULL) {Ttab = tmp; D("Setting Ttab = %s", Ttab);}
6591 if ((tmp = configs->getstr(configs, "webRoot", false)) != NULL) {webRoot = tmp; D("Setting webRoot = %s", webRoot);}
6592 if ((tmp = configs->getstr(configs, "URL", false)) != NULL) {URL = tmp; D("Setting URL = %s", URL);}
6593 if ((vd = configs->get (configs, "seshRenew", NULL, false)) != NULL) {seshRenew = (int) *((float *) vd); D("Setting seshRenew = %d", seshRenew);}
6594 if ((vd = configs->get (configs, "idleTimeOut", NULL, false)) != NULL) {idleTimeOut = (int) *((float *) vd); D("Setting idleTimeOut = %d", idleTimeOut);}
6595 if ((vd = configs->get (configs, "seshTimeOut", NULL, false)) != NULL) {seshTimeOut = (int) *((float *) vd); D("Setting seshTimeOut = %d", seshTimeOut);}
6596 if ((vd = configs->get (configs, "newbieTimeOut", NULL, false)) != NULL) {newbieTimeOut = (int) *((float *) vd); D("Setting newbieTimeOut = %d", newbieTimeOut);}
6597 if ((tmp = configs->getstr(configs, "ToS", false)) != NULL) {ToS = tmp; D("Setting ToS = %s", ToS);}
6598 if ((tmp = configs->getstr(configs, "webIframers", false)) != NULL) {webIframers = tmp; D("Setting webIframers = %s", webIframers);}
6599
6600
6601 // Use a FHS compatible setup -
6602 if (strcmp("/", scRoot) == 0)
6603 {
6604 scBin = "/bin";
6605 scEtc = "/etc/opensim_SC";
6606 scLib = "/usr/lib/opensim_SC";
6607 scRun = "/run/opensim_SC";
6608 scBackup = "/var/backups/opensim_SC";
6609 scCache = "/var/cache/opensim_SC";
6610 scData = "/var/lib/opensim_SC";
6611 scLog = "/var/log/opensim_SC";
6612 }
6613 else if (strcmp("/usr/local", scRoot) == 0)
6614 {
6615 scBin = "/usr/local/bin";
6616 scEtc = "/usr/local/etc/opensim_SC";
6617 scLib = "/usr/local/lib/opensim_SC";
6618 scRun = "/run/opensim_SC";
6619 scBackup = "/var/local/backups/opensim_SC";
6620 scCache = "/var/local/cache/opensim_SC";
6621 scData = "/var/local/lib/opensim_SC";
6622 scLog = "/var/local/log/opensim_SC";
6623 }
6624 else // A place for everything to live, like /opt/opensim_SC
6625 {
6626 char *slsh = "";
6627
6628 if ('/' != scRoot[0])
6629 {
6630 E("scRoot is not an absolute path - %s.", scRoot);
6631 slsh = "/";
6632 }
6633 // The problem here is that OpenSim already uses /opt/opensim_SC/current/bin for all of it's crap, where they mix everything together.
6634 // Don't want it in the path.
6635 // Could put the .dlls into lib. Most of the rest is config files or common assets.
6636 // Or just slowly migrate opensim stuff to FHS.
6637 scBin = xmprintf("%s%s/bin", slsh, scRoot);
6638 scEtc = xmprintf("%s%s/etc", slsh, scRoot);
6639 scLib = xmprintf("%s%s/lib", slsh, scRoot);
6640 scRun = xmprintf("%s%s/var/run", slsh, scRoot);
6641 scBackup = xmprintf("%s%s/var/backups", slsh, scRoot);
6642 scCache = xmprintf("%s%s/var/cache", slsh, scRoot);
6643 scData = xmprintf("%s%s/var/lib", slsh, scRoot);
6644 scLog = xmprintf("%s%s/var/log", slsh, scRoot);
6645 }
6646
6647
6648// TODO - still a bit chicken and egg here about the tmux socket and reading configs from scEtc /.sledjChisl.conf.lua
6649 if (!isWeb)
6650 {
6651 I("Outputting to a terminal, not a web server.");
6652 // Check if we are already running inside the proper tmux server.
6653 char *eTMUX = getenv("TMUX");
6654 memset(toybuf, 0, sizeof(toybuf));
6655 snprintf(toybuf, sizeof(toybuf), "%s/%s", scRun, Tsocket);
6656 if (((eTMUX) && (0 == strncmp(toybuf, eTMUX, strlen(toybuf)))))
6657 {
6658 I("Running inside the proper tmux server. %s", eTMUX);
6659 isTmux = TRUE;
6660 }
6661 else
6662 I("Not running inside the proper tmux server, starting it. %s == %s", eTMUX, toybuf);
6663 }
6664
6665 if (isTmux || isWeb)
6666 {
6667 char *d;
6668
6669 // Doing this here coz at this point we should be the correct user.
6670 if ((! qfile_exist(scBin)) && (! qfile_mkdir(scBin, S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP, true))) C("Unable to create path %s", scBin);
6671 if ((! qfile_exist(scEtc)) && (! qfile_mkdir(scEtc, S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP, true))) C("Unable to create path %s", scEtc);
6672 if ((! qfile_exist(scLib)) && (! qfile_mkdir(scLib, S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP, true))) C("Unable to create path %s", scLib);
6673 if ((! qfile_exist(scRun)) && (! qfile_mkdir(scRun, S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IWGRP | S_IXGRP | S_ISGID, true))) C("Unable to create path %s", scRun);
6674 if ((! qfile_exist(scBackup)) && (! qfile_mkdir(scBackup, S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP, true))) C("Unable to create path %s", scBackup);
6675// TODO - the path to scCache/sledjchisl.socket needs to be readable by the www-data group. So the FCGI socket will work.
6676// AND it needs to be group sticky on opensimsc group. So the tmux socket will work.
6677// So currently scCache is www-data readable, and scRun is group sticky.
6678 if ((! qfile_exist(scCache)) && (! qfile_mkdir(scCache, S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP, true))) C("Unable to create path %s", scCache);
6679 if ((! qfile_exist(scData)) && (! qfile_mkdir(scData, S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP, true))) C("Unable to create path %s", scData);
6680 if ((! qfile_exist(scLog)) && (! qfile_mkdir(scLog, S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP, true))) C("Unable to create path %s", scLog);
6681 tmp = xmprintf("%s/sessions", scCache);
6682 if ((! qfile_exist(tmp)) && (! qfile_mkdir(tmp, S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP, true))) C("Unable to create path %s", tmp);
6683 free(tmp);
6684 tmp = xmprintf("%s/users", scData);
6685 if ((! qfile_exist(tmp)) && (! qfile_mkdir(tmp, S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP, true))) C("Unable to create path %s", tmp);
6686 free(tmp);
6687
6688
6689 mimeTypes = qhashtbl(0, 0);
6690 mimeTypes->putstr(mimeTypes, "gz", "application/gzip");
6691 mimeTypes->putstr(mimeTypes, "js", "application/javascript");
6692 mimeTypes->putstr(mimeTypes, "json", "application/json");
6693 mimeTypes->putstr(mimeTypes, "pdf", "application/pdf");
6694 mimeTypes->putstr(mimeTypes, "rtf", "application/rtf");
6695 mimeTypes->putstr(mimeTypes, "zip", "application/zip");
6696 mimeTypes->putstr(mimeTypes, "xz", "application/x-xz");
6697 mimeTypes->putstr(mimeTypes, "gif", "image/gif");
6698 mimeTypes->putstr(mimeTypes, "png", "image/png");
6699 mimeTypes->putstr(mimeTypes, "jp2", "image/jp2");
6700 mimeTypes->putstr(mimeTypes, "jpg2", "image/jp2");
6701 mimeTypes->putstr(mimeTypes, "jpe", "image/jpeg");
6702 mimeTypes->putstr(mimeTypes, "jpg", "image/jpeg");
6703 mimeTypes->putstr(mimeTypes, "jpeg", "image/jpeg");
6704 mimeTypes->putstr(mimeTypes, "svg", "image/svg+xml");
6705 mimeTypes->putstr(mimeTypes, "svgz", "image/svg+xml");
6706 mimeTypes->putstr(mimeTypes, "tif", "image/tiff");
6707 mimeTypes->putstr(mimeTypes, "tiff", "image/tiff");
6708 mimeTypes->putstr(mimeTypes, "css", "text/css");
6709 mimeTypes->putstr(mimeTypes, "html", "text/html");
6710 mimeTypes->putstr(mimeTypes, "htm", "text/html");
6711 mimeTypes->putstr(mimeTypes, "shtml", "text/html");
6712// mimeTypes->putstr(mimeTypes, "md", "text/markdown");
6713// mimeTypes->putstr(mimeTypes, "markdown", "text/markdown");
6714 mimeTypes->putstr(mimeTypes, "txt", "text/plain");
6715
6716 memset(toybuf, 0, sizeof(toybuf));
6717 snprintf(toybuf, sizeof(toybuf), "%s/config/config.ini", scRoot);
6718
6719// TODO - it looks like OpenSim invented their own half arsed backwards INI file include system.
6720// I doubt qLibc supports it, like it supports what seems to be the standard include system.
6721// Not sure if we need to worry about it just yet.
6722// TODO - this leaks memory, but it's a bug in qLibc. Send the bug fix upstream.
6723 qlisttbl_t *qconfig = qconfig_parse_file(NULL, toybuf, '=');
6724 if (NULL == qconfig)
6725 {
6726 E("Can't read config file %s", toybuf);
6727 goto finished;
6728 }
6729 d = qstrunchar(qconfig->getstr(qconfig, "Const.ConnectionString", false), '"', '"');
6730
6731 if (NULL == d)
6732 {
6733 E("No database credentials in %s!", toybuf);
6734 goto finished;
6735 }
6736 else
6737 {
6738 char *p0, *p1, *p2;
6739 if (NULL == (d = strdup(d)))
6740 {
6741 E("Out of memory!");
6742 goto finished;
6743 }
6744 // Data Source=MYSQL_HOST;Database=MYSQL_DB;User ID=MYSQL_USER;Password=MYSQL_PASSWORD;Old Guids=true;
6745 p0 = d;
6746 while (NULL != p0)
6747 {
6748 p1 = strchr(p0, '=');
6749 if (NULL == p1) break;
6750 *p1 = '\0';
6751 p2 = strchr(p1 + 1, ';');
6752 if (NULL == p2) break;
6753 *p2 = '\0';
6754 configs->putstr(configs, p0, p1 + 1); // NOTE - this allocs memory for it's key and it's data.
6755 p0 = p2 + 1;
6756 if ('\0' == *p0)
6757 p0 = NULL;
6758 };
6759 free(d);
6760 }
6761
6762// TODO - should also load god names, and maybe the SMTP stuff.
6763// Note that the OpenSim SMTP stuff is ONLY for LSL script usage, we probably want to put it in the Lua config file instead.
6764
6765 if (mysql_library_init(toys.optc, toys.optargs, NULL))
6766 {
6767 E("mysql_library_init() failed!");
6768 goto finished;
6769 }
6770 if (!dbConnect())
6771 goto finished;
6772
6773 // Need to kick this off.
6774 stats = getStats(database, stats);
6775 char *h = qstrunchar(qconfig->getstr(qconfig, "Const.HostName", false), '"', '"');
6776 char *p = qstrunchar(qconfig->getstr(qconfig, "Const.PublicPort", false), '"', '"');
6777 stats->stats->putstr(stats->stats, "grid", qstrunchar(qconfig->getstr(qconfig, "Const.GridName", false), '"', '"'));
6778 stats->stats->putstr(stats->stats, "HostName", h);
6779 stats->stats->putstr(stats->stats, "PublicPort", p);
6780 snprintf(toybuf, sizeof(toybuf), "http://%s:%s/", h, p);
6781
6782 stats->stats->putstr(stats->stats, "uri", toybuf);
6783 qconfig->free(qconfig);
6784 }
6785
6786
6787 if (isWeb)
6788 {
6789 char **initialEnv = environ;
6790 char *tmp0, *tmp1;
6791 int count = 0, entries, bytes;
6792
6793 dynPages = qhashtbl(0, 0);
6794 newDynPage("account.html", (pageFunction) account_html);
6795
6796 // FCGI_LISTENSOCK_FILENO is the socket to the web server.
6797 // STDOUT and STDERR go to the web servers error log, or at least it does in Apache 2 mod_fcgid.
6798 I("Running SledjChisl inside a web server, pid %d.", getpid());
6799
6800 if (0 == toys.optc)
6801 D("no args");
6802 else
6803 {
6804 for (entries = 0, bytes = -1; entries < toys.optc; entries++)
6805 D("ARG %s\n", toys.optargs[entries]);
6806 }
6807 printEnv(environ);
6808
6809/*
6810? https://stackoverflow.com/questions/30707792/how-to-disable-buffering-with-apache2-and-mod-proxy-fcgi
6811 https://z-issue.com/wp/apache-2-4-the-event-mpm-php-via-mod_proxy_fcgi-and-php-fpm-with-vhosts/
6812 A lengthy and detailed "how to set this up with PHP" that might be useful.
6813 https://www.apachelounge.com/viewtopic.php?t=4385
6814 "Also the new mod_proxy_fcgi for Apache 2.4 seems to be crippled just like mod_fcgid in terms of being limited to just one request per process at a time."
6815 But then so is the silly fcgi2 SDK, which basically assumes it's a CGI wrapper, not proper FCGI.
6816+ I could even start the spawn-fcgi process from the tmux instance of sledjchisl.
6817+ Orrr just open the socket / port myself from the tmux instance and do the raw FCGI thing through it.
6818*/
6819 while (FCGI_Accept() != -1)
6820 {
6821 reqData *Rd = xzalloc(sizeof(reqData));
6822
6823 if (-1 == clock_gettime(CLOCK_REALTIME, &Rd->then))
6824 perror_msg("Unable to get the time.");
6825 Rd->L = L;
6826 Rd->configs = configs;
6827// Rd->queries = qhashtbl(0, 0); // Inited in toknize below.
6828// Rd->body = qhashtbl(0, 0); // Inited in toknize below.
6829// Rd->cookies = qhashtbl(0, 0); // Inited in toknize below.
6830 Rd->headers = qhashtbl(0, 0);
6831 Rd->valid = qhashtbl(0, 0);
6832 Rd->stuff = qhashtbl(0, 0);
6833 Rd->database = qhashtbl(0, 0);
6834 Rd->Rcookies = qhashtbl(0, 0);
6835 Rd->Rheaders = qhashtbl(0, 0);
6836 Rd->stats = stats;
6837 Rd->errors = qlist(0);
6838 Rd->messages = qlist(0);
6839 Rd->reply = qgrow(QGROW_THREADSAFE);
6840 Rd->outQuery = xstrdup("");
6841 Rd->shs.status = SHS_UNKNOWN;
6842 qhashtbl_obj_t hobj;
6843 qlist_obj_t lobj;
6844
6845 // So far I'm seeing these as all caps, but I don't think they are defined that way. Case insensitive per the spec.
6846 // So convert them now, also "-" -> "_".
6847t("HEADERS");
6848 char **envp = environ;
6849 for ( ; *envp != NULL; envp++)
6850 {
6851 char *k = xstrdup(*envp);
6852 char *v = strchr(k, '=');
6853
6854 if (NULL != v)
6855 {
6856 *v = '\0';
6857 char *ky = qstrreplace("tr", qstrupper(k), "-", "_");
6858 Rd->headers->putstr(Rd->headers, ky, v + 1);
6859if ((strcmp("HTTP_COOKIE", ky) == 0) || (strcmp("CONTENT_LENGTH", ky) == 0) || (strcmp("QUERY_STRING", ky) == 0))
6860 d(" %s = %s", ky, v + 1);
6861 }
6862 free(k);
6863 }
6864
6865 // The FCGI paramaters sent from the server, are converted to environment variablse for the fcgi2 SDK.
6866 // The FCGI spec doesn't mention what these are. except FCGI_WEB_SERVER_ADDRS.
6867 char *Role = Rd->headers->getstr(Rd->headers, "FCGI_ROLE", false);
6868 Rd->Path = Rd->headers->getstr(Rd->headers, "PATH_INFO", false);
6869// if (NULL == Rd->Path) {msleep(1000); continue;}
6870 char *Length = Rd->headers->getstr(Rd->headers, "CONTENT_LENGTH", false);
6871//char *Type = Rd->headers->getstr(Rd->headers, "CONTENT_TYPE", false);
6872 Rd->Method = Rd->headers->getstr(Rd->headers, "REQUEST_METHOD", false);
6873 Rd->Script = Rd->headers->getstr(Rd->headers, "SCRIPT_NAME", false);
6874 Rd->Scheme = Rd->headers->getstr(Rd->headers, "REQUEST_SCHEME", false);
6875 Rd->Host = Rd->headers->getstr(Rd->headers, "HTTP_HOST", false);
6876//char *SUri = Rd->headers->getstr(Rd->headers, "SCRIPT_URI", false);
6877 Rd->RUri = Rd->headers->getstr(Rd->headers, "REQUEST_URI", true);
6878//char *Cookies = Rd->headers->getstr(Rd->headers, "HTTP_COOKIE", false);
6879//char *Referer = Rd->headers->getstr(Rd->headers, "HTTP_REFERER", false);
6880//char *RAddr = Rd->headers->getstr(Rd->headers, "REMOTE_ADDR", false);
6881//char *Cache = Rd->headers->getstr(Rd->headers, "HTTP_CACHE_CONTROL", false);
6882//char *FAddrs = Rd->headers->getstr(Rd->headers, "FCGI_WEB_SERVER_ADDRS", false);
6883//char *Since = Rd->headers->getstr(Rd->headers, "IF_MODIFIED_SINCE", false);
6884 /* Per the spec CGI https://www.ietf.org/rfc/rfc3875
6885 meta-variable-name = "AUTH_TYPE" | "CONTENT_LENGTH" |
6886 "CONTENT_TYPE" | "GATEWAY_INTERFACE" |
6887 "PATH_INFO" | "PATH_TRANSLATED" |
6888 "QUERY_STRING" | "REMOTE_ADDR" |
6889 "REMOTE_HOST" | "REMOTE_IDENT" |
6890 "REMOTE_USER" | "REQUEST_METHOD" |
6891 "SCRIPT_NAME" | "SERVER_NAME" |
6892 "SERVER_PORT" | "SERVER_PROTOCOL" |
6893 "SERVER_SOFTWARE"
6894 Also protocol / scheme specific ones -
6895 HTTP_* comes from some of the request header. The rest are likely part of the other env variables.
6896 Also covers HTTPS headers, with the HTTP_* names.
6897
6898 */
6899
6900t("COOKIES");
6901 Rd->cookies = toknize(Rd->headers->getstr(Rd->headers, "HTTP_COOKIE", false), "=;");
6902 santize(Rd->cookies);
6903 if (strcmp("GET", Rd->Method) == 0)
6904 { // In theory a POST has body fields INSTEAD of query fields. Especially for ignoring the linky-hashish after the validation page.
6905t("QUERY");
6906 Rd->queries = toknize(Rd->headers->getstr(Rd->headers, "QUERY_STRING", false), "=&");
6907 santize(Rd->queries);
6908 }
6909 else
6910 {
6911T("ignoring QUERY");
6912 Rd->queries = qhashtbl(0, 0);
6913 free(Rd->RUri);
6914 Rd->RUri = xmprintf("%s%s", Rd->Script, Rd->Path);
6915 }
6916t("BODY");
6917 char *Body = NULL;
6918 if (Length != NULL)
6919 {
6920 size_t len = strtol(Length, NULL, 10);
6921 Body = xmalloc(len + 1);
6922 int c = FCGI_fread(Body, 1, len, FCGI_stdin);
6923 if (c != len)
6924 {
6925 E("Tried to read %d of the body, only got %d!", len, c);
6926 }
6927 Body[len] = '\0';
6928 }
6929 else
6930 Length = "0";
6931 Rd->body = toknize(Body, "=&");
6932 free(Body);
6933 santize(Rd->body);
6934
6935 I("%s %s://%s%s -> %s%s", Rd->Method, Rd->Scheme, Rd->Host, Rd->RUri, webRoot, Rd->Path);
6936 D("Started FCGI web request ROLE = %s, body is %s bytes, pid %d.", Role, Length, getpid());
6937
6938 if (NULL == Rd->Path)
6939 {
6940 E("NULL path in FCGI request!");
6941 Rd->Rheaders->putstr(Rd->Rheaders, "Status", "404 Not Found");
6942 goto sendReply;
6943 }
6944
6945/* TODO - other headers may include -
6946 different Content-type
6947 Status: 304 Not Modified
6948 Last-Modified: timedatemumble
6949 https://en.wikipedia.org/wiki/List_of_HTTP_header_fields
6950*/
6951 Rd->Rheaders->putstr(Rd->Rheaders, "Status", "200 OK");
6952 Rd->Rheaders->putstr(Rd->Rheaders, "Content-type", "text/html");
6953// TODO - check these.
6954 // This is all dynamic web pages, and likeley secure to.
6955 // Most of these are from https://www.smashingmagazine.com/2017/04/secure-web-app-http-headers/
6956 // https://www.twilio.com/blog/a-http-headers-for-the-responsible-developer is good to, and includes useful compression and image stuff.
6957 // On the other hand, .css files are referenced, which might be better off being cached, so should tweak some of thees.
6958 Rd->Rheaders->putstr(Rd->Rheaders, "Cache-Control", "no-cache, no-store, must-revalidate");
6959 Rd->Rheaders->putstr(Rd->Rheaders, "Pragma", "no-cache");
6960 Rd->Rheaders->putstr(Rd->Rheaders, "Expires", "-1");
6961 Rd->Rheaders->putstr(Rd->Rheaders, "Strict-Transport-Security", "max-age=63072000"); // Two years.
6962// TODO - do something about this -
6963 /* https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/style-src
6964 "Note: Disallowing inline styles and inline scripts is one of
6965 the biggest security wins CSP provides. However, if you
6966 absolutely have to use it, there are a few mechanisms that
6967 will allow them."
6968
6969 WTF? And the mechanisms include nonces, hashes, or 'unsafe-inline'.
6970 Not sure why inline styles need to be that secure, when downloaded ones are not.
6971 Ah, it's for user input that is sent back to other users, they might include funky CSS in their input.
6972 SOOOO, proper validation and escaping is needed.
6973 OOOOR, use the nonce, and make it a different nonce per page serve.
6974 OOOOR, just put all the style stuff in a .css file. Then we can use style-src 'self' without the 'unsafe-inline'?
6975 There's only one block of <style in the header I think.
6976 */
6977 // Content-Security-Policy can get complex, and I first wrote that when it was very simple. lol
6978 if ('\0' != webIframers[0])
6979 Rd->Rheaders->putstrf(Rd->Rheaders, "Content-Security-Policy",
6980 "default-src 'self'; script-src 'none'; form-action 'self'; style-src 'self' 'unsafe-inline'; frame-ancestors 'self' %s", webIframers);
6981 else
6982 {
6983 Rd->Rheaders->putstr(Rd->Rheaders, "Content-Security-Policy", "default-src 'self'; script-src 'none'; form-action 'self'; style-src 'self' 'unsafe-inline'");
6984 Rd->Rheaders->putstr(Rd->Rheaders, "X-Frame-Options", "SAMEORIGIN"); // This is deprecated, and is an all or nothing thing.
6985 }
6986 Rd->Rheaders->putstr(Rd->Rheaders, "X-XSS-Protection", "1;mode=block");
6987 Rd->Rheaders->putstr(Rd->Rheaders, "X-Content-Type-Options", "nosniff");
6988// Failed experiment, looks like JavaScript is the only way to change headers for the session ID.
6989// Rd->Rheaders->putstr(Rd->Rheaders, "X-Toke-N-Munchie", "foo, bar");
6990
6991 if ((strcmp("GET", Rd->Method) != 0) && (strcmp("HEAD", Rd->Method) != 0) && (strcmp("POST", Rd->Method) != 0))
6992 {
6993 E("Unsupported HTTP method %s", Rd->Method);
6994 Rd->Rheaders->putstr(Rd->Rheaders, "Status", "405 Method Not Allowed");
6995 goto sendReply;
6996 }
6997
6998 memset(toybuf, 0, sizeof(toybuf));
6999 snprintf(toybuf, sizeof(toybuf), "%s%s", webRoot, Rd->Path);
7000 HTMLfile *thisFile = checkHTMLcache(toybuf);
7001 if (NULL == thisFile)
7002 {
7003 dynPage *dp = dynPages->get(dynPages, &Rd->Path[1], NULL, false);
7004 if (NULL == dp)
7005 {
7006 E("Can't access file %s", toybuf);
7007 Rd->Rheaders->putstr(Rd->Rheaders, "Status", "404 Not Found");
7008 E("Failed to open %s, it's not a virtual file either", toybuf);
7009 goto sendReply;
7010 }
7011 dp->func(toybuf, Rd, thisFile);
7012 char *finl = Rd->reply->tostring(Rd->reply); // This mallocs new storage and returns it to us.
7013// TODO - maybe cache this?
7014 qlist_t *fragments = fragize(finl, Rd->reply->datasize(Rd->reply));
7015 Rd->reply->free(Rd->reply);
7016 Rd->reply = qgrow(QGROW_THREADSAFE);
7017 unfragize(fragments, Rd, TRUE);
7018 free(finl);
7019goto sendReply;
7020 }
7021
7022 tmp0 = qfile_get_ext(toybuf);
7023 tmp1 = mimeTypes->getstr(mimeTypes, tmp0, false);
7024 free(tmp0);
7025 if (NULL != tmp1)
7026 {
7027 if (strncmp("text/", tmp1, 5) != 0)
7028 {
7029 E("Only text formats are supported - %s", toybuf);
7030 Rd->Rheaders->putstr(Rd->Rheaders, "Status", "415 Unsupported Media Type");
7031 goto sendReply;
7032 }
7033 }
7034 else
7035 {
7036 E("Not actually a teapot, er I mean file has no extension, can't determine media type the easy way - %s", toybuf);
7037 Rd->Rheaders->putstr(Rd->Rheaders, "Status", "418 I'm a teapot");
7038 goto sendReply;
7039 }
7040
7041// Rd->Rheaders->putstr(Rd->Rheaders, "Last-Modified", thisFile->last.tv_sec);
7042// This is dynamic content, it's always gonna be modified. I think.
7043// if (NULL != Since)
7044// {
7045// time_t snc = qtime_parse_gmtstr(Since);
7046// TODO - should validate the time, log and ignore it if not valid.
7047// if (thisFile->last.tv_sec < snc)
7048// {
7049// D("Status: 304 Not Modified - %s", toybuf);
7050// setHeader("Status", "304 Not Modified");
7051// goto sendReply;
7052// }
7053// }
7054
7055 if (strcmp("HEAD", Rd->Method) == 0)
7056 goto sendReply;
7057
7058 getStats(database, stats);
7059 unfragize(thisFile->fragments, Rd, FALSE);
7060 free(thisFile);
7061
7062sendReply:
7063 /* Send headers.
7064 BTW, the Status header should be sent first I think.
7065 https://www.ietf.org/rfc/rfc3875 6.2 says order isn't important.
7066 It even says Status is optional, 200 is assumed. Content-Type is mandatory.
7067 8.2 "Recommendations for Scripts" is worth complying with.
7068 9 "Security Considerations"
7069 https://tools.ietf.org/html/rfc7230 3.1.2 says status line must be first. lol
7070 */
7071 FCGI_printf("Status: %s\r\n", getStrH(Rd->Rheaders, "Status"));
7072 memset((void *) &hobj, 0, sizeof(hobj));
7073 Rd->Rheaders->lock(Rd->Rheaders);
7074 while (Rd->Rheaders->getnext(Rd->Rheaders, &hobj, false) == true)
7075 {
7076 if (strcmp("Status", (char *) hobj.name) != 0)
7077 FCGI_printf("%s: %s\r\n", (char *) hobj.name, (char *) hobj.data);
7078 }
7079 Rd->Rheaders->unlock(Rd->Rheaders);
7080 // Send cookies.
7081 memset((void *) &hobj, 0, sizeof(hobj));
7082 Rd->Rcookies->lock(Rd->Rcookies);
7083 while (Rd->Rcookies->getnext(Rd->Rcookies, &hobj, false) == true)
7084 {
7085 cookie *ck = (cookie *) hobj.data;
7086 FCGI_printf("Set-Cookie: %s=%s", hobj.name, ck->value);
7087// if (NULL != ck->expires) FCGI_printf("; Expires=%s", ck->expires);
7088 if (NULL != ck->domain) FCGI_printf("; Domain=%s", ck->domain);
7089 if (NULL != ck->path) FCGI_printf("; Path=%s", ck->path);
7090 if (0 != ck->maxAge) FCGI_printf("; Max-Age=%d", ck->maxAge);
7091 if ( ck->secure) FCGI_printf("; Secure");
7092 if ( ck->httpOnly) FCGI_printf("; HttpOnly");
7093 if (CS_STRICT == ck->site) FCGI_printf("; SameSite=Strict");
7094 if (CS_LAX == ck->site) FCGI_printf("; SameSite=Lax");
7095 if (CS_NONE == ck->site) FCGI_printf("; SameSite=None");
7096 FCGI_printf("\r\n");
7097 free(ck->value);
7098 ck->value = NULL;
7099 }
7100 FCGI_printf("\r\n");
7101 Rd->cookies->unlock(Rd->cookies);
7102 // Send body.
7103 char *final = Rd->reply->tostring(Rd->reply);
7104 if (NULL == final)
7105 {
7106 tmp0 = Rd->Rheaders->getstr(Rd->Rheaders, "Status", false);
7107 if (NULL == tmp0)
7108 {
7109 E("Some sort of error happpened! Status: UNKNOWN!!");
7110 FCGI_printf("Some sort of error happpened! Status: UNKNOWN!!");
7111 }
7112 else
7113 {
7114 E("Some sort of error happpened! Status: %s", tmp0);
7115 FCGI_printf("Some sort of error happpened! Status: %s", tmp0);
7116 }
7117 }
7118 else
7119 {
7120 FCGI_printf("%s", final);
7121 free(final);
7122 }
7123
7124fcgiDone:
7125 FCGI_Finish();
7126 if (NULL != Rd->outQuery) free(Rd->outQuery);
7127 if (NULL != Rd->shs.name) free(Rd->shs.name);
7128 Rd->shs.name = NULL;
7129 if (NULL != Rd->shs.UUID) free(Rd->shs.UUID);
7130 Rd->shs.UUID = NULL;
7131 qgrow_free(Rd->reply);
7132 qlist_free(Rd->messages);
7133 qlist_free(Rd->errors);
7134 qhashtbl_free(Rd->Rheaders);
7135 qhashtbl_free(Rd->Rcookies);
7136 qhashtbl_free(Rd->database);
7137 qhashtbl_free(Rd->stuff);
7138 qhashtbl_free(Rd->valid);
7139 qhashtbl_free(Rd->headers);
7140 qhashtbl_free(Rd->cookies);
7141 qhashtbl_free(Rd->body);
7142 qhashtbl_free(Rd->queries);
7143 if (Rd->lnk) free(Rd->lnk);
7144 free(Rd->RUri);
7145
7146 struct timespec now;
7147 if (-1 == clock_gettime(CLOCK_REALTIME, &now))
7148 perror_msg("Unable to get the time.");
7149 double n = (now.tv_sec * 1000000000.0) + now.tv_nsec;
7150 double t = (Rd->then.tv_sec * 1000000000.0) + Rd->then.tv_nsec;
7151 I("Finished web request, took %lf seconds", (n - t) / 1000000000.0);
7152 free(Rd);
7153 }
7154
7155 FCGI_fprintf(FCGI_stderr, "Stopped SledjChisl web server.\n");
7156 D("Stopped SledjChisl web server.");
7157
7158 goto finished;
7159 }
7160
7161
7162 if (!isTmux)
7163 { // Let's see if the proper tmux server is even running.
7164 memset(toybuf, 0, sizeof(toybuf));
7165 snprintf(toybuf, sizeof(toybuf), "%s %s/%s -q list-sessions 2>/dev/null | grep -q %s:", Tcmd, scRun, Tsocket, Tconsole);
7166 i = system(toybuf);
7167 if (WIFEXITED(i))
7168 {
7169 if (0 != WEXITSTATUS(i)) // No such sesion, create it.
7170 {
7171 memset(toybuf, 0, sizeof(toybuf));
7172 // The sudo is only so that the session is owned by opensim, otherwise it's owned by whoever ran this script, which is a likely security hole.
7173 // After the session is created, we rely on the scRun directory to be group sticky, so that anyone in the opensim group can attach to the tmux socket.
7174 snprintf(toybuf, sizeof(toybuf),
7175 "sudo -Hu %s %s %s/%s new-session -d -s %s -n '%s' \\; split-window -bhp 50 -t '%s:' bash -c '/usr/bin/valgrind --leak-check=full ./sledjchisl; cd %s; bash'",
7176 scUser, Tcmd, scRun, Tsocket, Tconsole, Ttab, Tconsole, scRoot);
7177 i = system(toybuf);
7178 if (!WIFEXITED(i))
7179 E("tmux new-session command failed! %s", toybuf);
7180 }
7181 // Join the session.
7182 memset(toybuf, 0, sizeof(toybuf));
7183 snprintf(toybuf, sizeof(toybuf), "%s %s/%s select-window -t '%s' \\; attach-session -t '%s'", Tcmd, scRun, Tsocket, Tconsole, Tconsole);
7184 i = system(toybuf);
7185 if (!WIFEXITED(i))
7186 E("tmux attach-session command failed! %s", toybuf);
7187 goto finished;
7188 }
7189 else
7190 E("tmux list-sessions command failed! %s", toybuf);
7191 }
7192
7193
7194 simList *sims = getSims();
7195 if (1)
7196 {
7197 struct sysinfo info;
7198 float la;
7199
7200 sysinfo(&info);
7201 la = info.loads[0]/65536.0;
7202
7203 if (!checkSimIsRunning("ROBUST"))
7204 {
7205 char *d = xmprintf("%s.{right}", Ttab);
7206 char *c = xmprintf("cd %s/current/bin", scRoot);
7207
7208 I("ROBUST is starting up.");
7209 sendTmuxCmd(d, c);
7210 free(c);
7211 c = xmprintf("mono Robust.exe -inidirectory=%s/config/ROBUST", scRoot);
7212 sendTmuxCmd(d, c);
7213 free(c);
7214 waitTmuxText(d, "INITIALIZATION COMPLETE FOR ROBUST");
7215 I("ROBUST is done starting up.");
7216 la = waitLoadAverage(la, loadAverageInc / 3.0, simTimeOut / 3);
7217 free(d);
7218 }
7219
7220 for (i = 0; i < sims->num; i++)
7221 {
7222 char *sim = sims->sims[i], *name = getSimName(sims->sims[i]);
7223
7224 if (!checkSimIsRunning(sim))
7225 {
7226 char *nm = cleanSimName(name);
7227
7228 I("%s is starting up.", name);
7229 memset(toybuf, 0, sizeof(toybuf));
7230 snprintf(toybuf, sizeof(toybuf), "%s %s/%s new-window -dn '%s' -t '%s:%d' 'cd %s/current/bin; mono OpenSim.exe -inidirectory=%s/config/%s'",
7231 Tcmd, scRun, Tsocket, nm, Tconsole, i + 1, scRoot, scRoot, sim);
7232 int r = system(toybuf);
7233 if (!WIFEXITED(r))
7234 E("tmux new-window command failed!");
7235 else
7236 {
7237 memset(toybuf, 0, sizeof(toybuf));
7238 snprintf(toybuf, sizeof(toybuf), "INITIALIZATION COMPLETE FOR %s", name);
7239 waitTmuxText(nm, toybuf);
7240 I("%s is done starting up.", name);
7241 la = waitLoadAverage(la, loadAverageInc, simTimeOut);
7242 }
7243 free(nm);
7244 }
7245 free(name);
7246 }
7247
7248 }
7249 else if (!strcmp(cmd, "create")) // "create name x,y size"
7250 {
7251 }
7252 else if (!strcmp(cmd, "start")) // "start sim01" "start Welcome" "start" start everything
7253 {
7254// TODO - check if sim is down, but tmux window is still up, and close the tmux window first.
7255 }
7256 else if (!strcmp(cmd, "backup")) // "backup onefang rejected" "backup sim01" "backup Welcome" "backup" backup everything
7257 { // If it's not a sim code, and not a sim name, it's an account inventory.
7258 }
7259 else if (!strcmp(cmd, "gitAR")) // "gitAR i name"
7260 {
7261 }
7262 else if (!strcmp(cmd, "stop")) // "stop sim01" "stop Welcome" "stop" stop everything
7263 {
7264 }
7265
7266 freeSimList(sims);
7267
7268/*
7269 double sum;
7270
7271 // Load the file containing the script we are going to run
7272 status = luaL_loadfile(L, "script.lua");
7273 if (status)
7274 {
7275 // If something went wrong, error message is at the top of the stack
7276 E("Couldn't load file: %s", lua_tostring(L, -1));
7277 goto finished;
7278 }
7279*/
7280
7281 /*
7282 * Ok, now here we go: We pass data to the lua script on the stack.
7283 * That is, we first have to prepare Lua's virtual stack the way we
7284 * want the script to receive it, then ask Lua to run it.
7285 */
7286// lua_newtable(L); /* We will pass a table */
7287
7288 /*
7289 * To put values into the table, we first push the index, then the
7290 * value, and then call lua_rawset() with the index of the table in the
7291 * stack. Let's see why it's -3: In Lua, the value -1 always refers to
7292 * the top of the stack. When you create the table with lua_newtable(),
7293 * the table gets pushed into the top of the stack. When you push the
7294 * index and then the cell value, the stack looks like:
7295 *
7296 * <- [stack bottom] -- table, index, value [top]
7297 *
7298 * So the -1 will refer to the cell value, thus -3 is used to refer to
7299 * the table itself. Note that lua_rawset() pops the two last elements
7300 * of the stack, so that after it has been called, the table is at the
7301 * top of the stack.
7302 */
7303/*
7304 for (i = 1; i <= 5; i++)
7305 {
7306 lua_pushnumber(L, i); // Push the table index
7307 lua_pushnumber(L, i*2); // Push the cell value
7308 lua_rawset(L, -3); // Stores the pair in the table
7309 }
7310
7311 // By what name is the script going to reference our table?
7312 lua_setglobal(L, "foo");
7313
7314 // Ask Lua to run our little script
7315 result = lua_pcall(L, 0, LUA_MULTRET, 0);
7316 if (result)
7317 {
7318 E("Failed to run script: %s", lua_tostring(L, -1));
7319 goto finished;
7320 }
7321
7322 // Get the returned value at the top of the stack (index -1)
7323 sum = lua_tonumber(L, -1);
7324
7325 I("Script returned: %.0f", sum);
7326
7327 lua_pop(L, 1); // Take the returned value out of the stack
7328*/
7329
7330finished:
7331
7332 // An example of calling a toy directly.
7333// printf("\n\n");
7334// char *argv[] = {"ls", "-l", "-a", NULL};
7335// printf("%d\n", runToy(argv));
7336
7337
7338 puts("This is the end my friend.");
7339 fflush(stdout);
7340
7341 cleanup();
7342}
diff --git a/src/tickle.lua b/src/tickle.lua
new file mode 100755
index 0000000..fc89237
--- /dev/null
+++ b/src/tickle.lua
@@ -0,0 +1,26 @@
1#!/usr/bin/env luajit
2
3-- Grab the stats web page, just to ping the database server, coz sledjchisl isn't event driven yet.
4
5local paths =
6{
7 ".sledjChisl.conf.lua",
8 "/etc/sledjChisl.conf.lua",
9-- "/etc/sledjChisl.d/*.lua",
10 "~/.sledjChisl.conf.lua",
11-- "~/.config/sledjChisl/*.lua",
12}
13
14local config = {}
15
16for k, v in ipairs(paths) do
17 local cfg = loadfile(v)
18 if nil ~= cfg then
19 cfg = cfg()
20 for k, w in pairs(cfg) do
21 config[k] = w
22 end
23 end
24end
25
26os.execute("curl -so /dev/null http://" .. config.webHost .. "/" .. config.URL .. "/stats.html")