├── .gitignore ├── MANIFEST ├── README.md ├── action-script ├── base.conf ├── bsdinstall-scripted ├── httpd ├── restapi ├── screenshots ├── add-user-2021-08-04.png └── main-page-2021-08-04.png ├── server ├── boot.lua ├── db.lua ├── disk.lua ├── filesystem.lua ├── hardening.lua ├── httpd.lua ├── install.lua ├── json.lua ├── keymap.lua ├── lang.lua ├── misc.lua ├── network.lua ├── partition.lua ├── pkgsets.lua ├── service.lua ├── shell.lua └── template.lua ├── style.css └── views ├── add_user.html ├── keymap.html ├── lang.html ├── main_page.html ├── network.html └── zfs.html /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | db 3 | *.swp 4 | -------------------------------------------------------------------------------- /MANIFEST: -------------------------------------------------------------------------------- 1 | base-dbg.txz 66e73ce96d481f7baff4d97085822aca57a54fbdaabbb717cbeb94a903c9bd36 1643 base_dbg "Base system (Debugging)" off 2 | base.txz 791e5a94284f4cbb7c8c6ba07c11c2fb61c50a3cde67d1235734d4db7572f657 26852 base "Base system (MANDATORY)" on 3 | kernel-dbg.txz 2c8eae5dbab073659c23afc9249c6aefa61f0b3f8b3f2329c37de35a25bb8efc 814 kernel_dbg "Kernel (Debugging)" on 4 | kernel.txz 73e910994a9977d5dd6eda8b6542236bf78ddcf4867935dfcbbc3f5d6765b87c 825 kernel "Kernel (MANDATORY)" on 5 | lib32-dbg.txz bc2f5c9016ee5f15c8637f7a8e9fdba44f3c4fce7d91c4d156ac33a62cc2677b 247 lib32_dbg "32-bit compatibility libraries (Debugging)" off 6 | lib32.txz 6d7a73c6e4f3e2b216f53be61872af926a1e0d54979ebec3572c09044e78cafb 1034 lib32 "32-bit compatibility libraries" on 7 | ports.txz 5515859e0ed3b0e99d8702aff7233ad35110bb5302cf1e91022d8d1afc3530ed 178109 ports "Ports tree" off 8 | src.txz a4972cd59e3695d8f0fce7ae55fa15babb64f99f8f0a20498f07d0b0efdf85e5 94789 src "System source tree" off 9 | tests.txz 3de24ff516016bdd3462024e0c70c3a5a50cbb7135019cb6b3dd8ddbe7f408bf 6573 tests "Test suite" off 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Experimental FreeBSD Installer 2 | 3 | Building on the work of [lua-httpd](https://gitlab.com/freqlabs/lua-httpd/-/tree/freebsd-install). 4 | 5 | ## Setup 6 | 7 | The easiest way to try out the installer is to use the [Live ISO builder](https://github.com/yangzhong-freebsd/ISO) that has the installer pre-configured in it. 8 | 9 | Or, for testing purposes, I have the server running on an already-installed 10 | system. I would suggest not to set this up on an important computer as the 11 | server runs as root. To set up (as you can see, it's currently very rough): 12 | 13 | Clone this repository, and make an empty file named `db` in it. 14 | 15 | Configure inetd, then restart it: 16 | `/etc/inetd.conf` 17 | ```conf 18 | http stream tcp nowait root {location of this repository}/httpd httpd 19 | ``` 20 | 21 | Prepare the log file: 22 | ```sh 23 | touch /var/log/httpd.log 24 | ``` 25 | 26 | Finally, edit the file `httpd`: change the value of the variable 27 | `SRC_DIR` to be the path to this repository, with the trailing slash. Also change 28 | `XAUTHORITY` to point at your .Xauthority file. 29 | 30 | Now you should be able to go to localhost on your browser and use the installer 31 | frontend! 32 | 33 | ## Problems and future plans 34 | 35 | 1. Currently, the network selector only supports wireless interfaces with WPA2. 36 | 2. The partitioner is also very basic and only supports ZFS. 37 | 3. bsdinstall offers many different ways to configure partitions, one way being to open a terminal and manually do it. This works because bsdinstall does the partitioning immediately after partitions have been configured. In the experimental installer, the partitioning options get written to the configuration file, and everything is done at the end, all at once. It doesn't seem possible to offer the manual partitioning option while keeping this property of the experimental installer. 38 | 4. Because the keymap configurator in the installer sets keymap on demand, it sets the X keymap but not the console one. I intend to add the option to install a graphical environment in the installer, but haven't done that work yet. So, if you change the layout, it'll be set for the rest of the installation process, but not in the final installed system which boots to the console. There does not seem to be a straightforward way to map X keymap/variant options to console keymap layouts. 39 | 40 | 81 | -------------------------------------------------------------------------------- /action-script: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | case "$2" in 4 | CONNECTED) 5 | echo "CONNECTED" > /tmp/lua-httpd; 6 | ;; 7 | DISCONNECTED) 8 | echo "DISCONNECTED" > /tmp/lua-httpd; 9 | ;; 10 | CTRL-EVENT-SSID-TEMP-DISABLED) 11 | echo "TEMP-DISABLED" > /tmp/lua-httpd; 12 | ;; 13 | esac 14 | -------------------------------------------------------------------------------- /base.conf: -------------------------------------------------------------------------------- 1 | FreeBSD-base: { 2 | url: "https://alpha.pkgbase.live/release/FreeBSD:13:amd64/latest/", 3 | signature_type: "pubkey", 4 | pubkey: "/usr/share/keys/pkg/trusted/alpha.pkgbase.live.pub", 5 | enabled: yes 6 | } 7 | -------------------------------------------------------------------------------- /bsdinstall-scripted: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | #- 3 | # Copyright (c) 2013 Nathan Whitehorn 4 | # Copyright (c) 2013-2015 Devin Teske 5 | # All rights reserved. 6 | # 7 | # Redistribution and use in source and binary forms, with or without 8 | # modification, are permitted provided that the following conditions 9 | # are met: 10 | # 1. Redistributions of source code must retain the above copyright 11 | # notice, this list of conditions and the following disclaimer. 12 | # 2. Redistributions in binary form must reproduce the above copyright 13 | # notice, this list of conditions and the following disclaimer in the 14 | # documentation and/or other materials provided with the distribution. 15 | # 16 | # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 17 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 19 | # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 20 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 22 | # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 23 | # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 24 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 25 | # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 26 | # SUCH DAMAGE. 27 | # 28 | # $FreeBSD$ 29 | # 30 | ############################################################ INCLUDES 31 | 32 | BSDCFG_SHARE="/usr/share/bsdconfig" 33 | . $BSDCFG_SHARE/common.subr || exit 1 34 | f_dprintf "%s: loading includes..." "$0" 35 | f_include $BSDCFG_SHARE/variable.subr #TODO figure out what this is 36 | 37 | ############################################################ CONFIGURATION 38 | 39 | # VARIABLES: 40 | # PARTITIONS 41 | # DISTRIBUTIONS 42 | # BSDINSTALL_DISTDIR 43 | 44 | # 45 | # Default name of the ZFS boot-pool 46 | # 47 | : ${ZFSBOOT_POOL_NAME:=zroot} 48 | 49 | : ${TMPDIR="/tmp"}; export TMPDIR 50 | 51 | : ${BSDINSTALL_TMPETC="${TMPDIR}/bsdinstall_etc"}; export BSDINSTALL_TMPETC 52 | : ${BSDINSTALL_TMPBOOT="${TMPDIR}/bsdinstall_boot"}; export BSDINSTALL_TMPBOOT 53 | : ${PATH_FSTAB="$BSDINSTALL_TMPETC/fstab"}; export PATH_FSTAB 54 | : ${BSDINSTALL_DISTDIR="/usr/freebsd-dist"}; export BSDINSTALL_DISTDIR 55 | : ${BSDINSTALL_CHROOT="/mnt"}; export BSDINSTALL_CHROOT 56 | 57 | ############################################################ GLOBALS 58 | 59 | 60 | # 61 | # Strings that should be moved to an i18n file and loaded with f_include_lang() 62 | # 63 | msg_installation_error="Installation Error!" 64 | 65 | ############################################################ FUNCTIONS 66 | 67 | error() 68 | { 69 | [ -f "$PATH_FSTAB" ] || exit 70 | if [ "$ZFSBOOT_DISKS" ]; then 71 | zpool export $ZFSBOOT_POOL_NAME 72 | else 73 | bsdinstall umount 74 | fi 75 | 76 | exit 1 77 | } 78 | 79 | ############################################################ MAIN 80 | 81 | set -e 82 | trap error EXIT 83 | 84 | SCRIPT="$1" 85 | shift 86 | 87 | f_dprintf "Began Installation at %s" "$( date )" 88 | rm -rf $BSDINSTALL_TMPETC 89 | mkdir $BSDINSTALL_TMPETC 90 | 91 | split -a 2 -p '^#!.*' "$SCRIPT" $TMPDIR/bsdinstall-installscript- 92 | 93 | . $TMPDIR/bsdinstall-installscript-aa 94 | : ${DISTRIBUTIONS="kernel.txz base.txz"}; export DISTRIBUTIONS 95 | export BSDINSTALL_DISTDIR 96 | 97 | # Re-initialize a new log if preamble changed BSDINSTALL_LOG 98 | if [ "$BSDINSTALL_LOG" != "${debugFile#+}" ]; then 99 | export debugFile="$BSDINSTALL_LOG" 100 | f_quietly f_debug_init 101 | # NB: Being scripted, let debug go to terminal for invalid debugFile 102 | f_dprintf "Began Installation at %s" "$( date )" 103 | fi 104 | 105 | # Make partitions 106 | rm -f $PATH_FSTAB 107 | touch $PATH_FSTAB 108 | if [ "$ZFSBOOT_DISKS" ]; then 109 | bsdinstall zfsboot 110 | else 111 | bsdinstall scriptedpart "$PARTITIONS" 112 | fi 113 | bsdinstall mount 114 | 115 | fetch --output=/usr/share/keys/pkg/trusted/alpha.pkgbase.live.pub https://alpha.pkgbase.live/alpha.pkgbase.live.pub 116 | 117 | # pkg -o ASSUME_ALWAYS_YES=yes -r ${BSDINSTALL_CHROOT} update &1 199 | rm $BSDINSTALL_CHROOT/tmp/installscript 200 | fi 201 | 202 | bsdinstall entropy 203 | #if [ "$ZFSBOOT_DISKS" ]; then 204 | # zpool export $ZFSBOOT_POOL_NAME 205 | #else 206 | # bsdinstall umount 207 | #fi 208 | 209 | f_dprintf "Installation Completed at %s" "$( date )" 210 | 211 | trap - EXIT 212 | exit $SUCCESS 213 | 214 | ################################################################################ 215 | # END 216 | ################################################################################ 217 | -------------------------------------------------------------------------------- /httpd: -------------------------------------------------------------------------------- 1 | #!/usr/libexec/flua 2 | -- vim: set et: 3 | -- Minimal web server written in Lua 4 | -- 5 | -- Use with inetd, no other dependencies: 6 | -- http stream tcp nowait root /usr/local/sbin/httpd httpd 7 | 8 | -- 9 | -- Copyright (c) 2016 - 2020 Ryan Moeller 10 | -- 11 | -- Permission to use, copy, modify, and distribute this software for any 12 | -- purpose with or without fee is hereby granted, provided that the above 13 | -- copyright notice and this permission notice appear in all copies. 14 | -- 15 | -- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 16 | -- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 17 | -- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 18 | -- ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 19 | -- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 20 | -- ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 21 | -- OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 22 | 23 | SRC_DIR = "/home/yang/src/lua-httpd/" 24 | XAUTHORITY = "/home/yang/.Xauthority" 25 | 26 | local VIEWS_DIR = SRC_DIR .. "views/" 27 | 28 | package.path = SRC_DIR .. "server/?.lua;" .. SRC_DIR .. "?.lua;" .. package.path 29 | 30 | local httpd = require("httpd") 31 | local template = require("template") 32 | 33 | local boot = require("boot") 34 | local db = require("db") 35 | local disk = require("disk") 36 | local filesystem = require("filesystem") 37 | local hardening = require("hardening") 38 | local install = require("install") 39 | local keymap = require("keymap") 40 | local lang = require("lang") 41 | local network = require("network") 42 | local pkgsets = require("pkgsets") 43 | local partition = require("partition") 44 | local service = require("service") 45 | local shell = require("shell") 46 | 47 | local json = require("json") 48 | local misc = require("misc") 49 | 50 | local stylesheet_file = assert(io.open(SRC_DIR .. "style.css", "r")) 51 | local stylesheet = stylesheet_file:read("*all") 52 | stylesheet_file:close() 53 | 54 | local main_page_file = assert(io.open(VIEWS_DIR .. "main_page.html", "r")) 55 | local main_page = main_page_file:read("*all") 56 | main_page_file:close() 57 | 58 | local add_user_file= assert(io.open(VIEWS_DIR .. "add_user.html", "r")) 59 | local add_user_page = add_user_file:read("*all") 60 | add_user_file:close() 61 | 62 | local network_file = assert(io.open(VIEWS_DIR .. "network.html", "r")) 63 | local network_page = network_file:read("*all") 64 | network_file:close() 65 | 66 | local keymap_file = assert(io.open(VIEWS_DIR .. "keymap.html", "r")) 67 | local keymap_page = keymap_file:read("*all") 68 | keymap_file:close() 69 | 70 | local zfs_file = assert(io.open(VIEWS_DIR .. "zfs.html", "r")) 71 | local zfs_page = zfs_file:read("*all") 72 | zfs_file:close() 73 | 74 | local language_file = assert(io.open(VIEWS_DIR .. "lang.html", "r")) 75 | local language_page = language_file:read("*all") 76 | language_file:close() 77 | 78 | function pairsByKeys(t, f) 79 | local a = {} 80 | for n in pairs(t) do table.insert(a, n) end 81 | table.sort(a, f) 82 | local i = 0 -- iterator variable 83 | local iter = function() -- iterator function 84 | i = i + 1 85 | if a[i] == nil then return nil 86 | else return a[i], t[a[i]] 87 | end 88 | end 89 | return iter 90 | end 91 | 92 | function selected(cond) 93 | return cond and "selected" or "" 94 | end 95 | 96 | function checked(cond) 97 | return cond and "checked" or "" 98 | end 99 | 100 | function readonly(cond) 101 | return cond and "readonly" or "" 102 | end 103 | 104 | function wireless(cond) 105 | return cond and "[Wireless]" or "[Wired]" 106 | end 107 | 108 | function disabled(cond) 109 | return cond and "disabled" or "" 110 | end 111 | 112 | function orEmpty(string) 113 | if (string) then 114 | return string 115 | else 116 | return "" 117 | end 118 | -- return string and string or "" 119 | end 120 | 121 | function orWheel(string) 122 | if (string) then 123 | return string 124 | else 125 | return "wheel" 126 | end 127 | -- return string and string or "" 128 | end 129 | 130 | --TODO: properly credit 131 | function unescape (str) 132 | str = string.gsub (str, "+", " ") 133 | str = string.gsub (str, "%%(%x%x)", function(h) return string.char(tonumber(h,16)) end) 134 | return str 135 | end 136 | 137 | function splitString(str, char) 138 | local split_list = {} 139 | 140 | for match in str:gmatch("[^" .. char .. "]+") do 141 | table.insert(split_list, match) 142 | end 143 | return split_list 144 | end 145 | 146 | function parseRequest(body) 147 | local lines = splitString(body, "&") 148 | local req = {} 149 | 150 | for i, line in ipairs(lines) do 151 | local mapping = unescape(line) 152 | local key, val = mapping:match("^([^=]+)=(.*)") 153 | 154 | --requests could possibly have multiple vals per key. 155 | --Since I know that these will always be for inputs 156 | --that will never have space in them (currently: disks) 157 | --I'll just space-separate them. 158 | if (type(req[key]) == "string") then 159 | req[key] = req[key].." "..val 160 | else 161 | req[key] = val 162 | end 163 | end 164 | 165 | return req 166 | end 167 | 168 | function getStylesheet() 169 | return { status=200, reason="ok", body=stylesheet } 170 | end 171 | 172 | function prefillDB() 173 | db.updateIfUnset("installer_language", "en") 174 | db.updateIfUnset("keymap_layout", keymap.getCurrentLayout()) 175 | db.updateIfUnset("keymap_variant", keymap.getCurrentVariant()) 176 | db.updateIfUnset("hostname", "freebsd") 177 | db.updateIfUnset("packages", "base kernel") --TODO make this depend on data in package.lua 178 | end 179 | 180 | function getSelectedDisks(parsed_db) 181 | local db_disks = parsed_db.zfs_disks or "" 182 | local selected_disk_list = misc.splitString(db_disks, " ") 183 | 184 | local selected_disks = {} 185 | for _, disk in ipairs(selected_disk_list) do 186 | selected_disks[disk] = true 187 | end 188 | 189 | return selected_disks 190 | end 191 | 192 | function mainPage(request) 193 | 194 | prefillDB() 195 | 196 | local parsed_db = db.parse() 197 | 198 | network.startDaemon() 199 | 200 | local cur_lang = parsed_db.installer_language 201 | local keymap_layout = parsed_db.keymap_layout 202 | local keymap_variant = parsed_db.keymap_variant 203 | 204 | local selected_packages_list = misc.splitString(parsed_db.packages, " ") 205 | local selected_packages = {} 206 | for _, package in ipairs(selected_packages_list) do 207 | selected_packages[package] = true 208 | end 209 | 210 | local installReady = (parsed_db.network and parsed_db.zfs_disks) --TODO: make this check smarter 211 | 212 | local body = template.process(main_page, 213 | { 214 | boothowto = boot.howto(), 215 | bootmethod = boot.method(), 216 | cur_lang = cur_lang, 217 | disks = disk.info(), 218 | filesystem_formats = filesystem.formats, 219 | hardening_menu = hardening.menu, 220 | hostname = parsed_db.hostname, 221 | keymap_string = keymap.prettyPrint(keymap_layout, keymap_variant), 222 | lang = lang.translations, 223 | network_string = parsed_db.network, 224 | packages = pkgsets.pkgsets, 225 | partition_styles = partition.styles, 226 | ready = installReady, 227 | shells = shell.list, 228 | selected_disks = getSelectedDisks(parsed_db), 229 | selected_packages = selected_packages, 230 | service_menu = service.menu, 231 | users = db.getUsersAsList(parsed_db), 232 | }) 233 | return { status=200, reason="ok", body=body } 234 | end 235 | 236 | function writePackages(request) --TODO make 'packages' 'pkgsets' naming more consistent 237 | local req = json.decode(request.body) 238 | db.update("packages", table.concat(req, " ")) 239 | return { status=200, reason="ok", body="TODO"} 240 | end 241 | 242 | function writeHostname(request) --TODO validate input 243 | db.update("hostname", request.body) 244 | return { status=200, reason="ok", body="TODO"} 245 | end 246 | 247 | function languagePage(request) 248 | local parsed_db = db.parse() 249 | local cur_lang = parsed_db.installer_language 250 | 251 | local body = template.process(language_page, 252 | { 253 | cur_lang = cur_lang, 254 | lang = lang.translations, 255 | languages = lang.languages, 256 | }) 257 | 258 | return { status=200, reason="ok", body=body } 259 | end 260 | 261 | function zfsPage(request) 262 | local parsed_db = db.parse() 263 | local selected_filesystem = parsed_db.zfs_filesystem or "stripe" 264 | local cur_lang = parsed_db.installer_language 265 | 266 | local body = template.process(zfs_page, 267 | { 268 | cur_lang = cur_lang, 269 | disks = disk.info(), 270 | filesystem_formats = filesystem.zfs_formats, 271 | lang = lang.translations, 272 | selected_disks = getSelectedDisks(parsed_db), 273 | selected_filesystem = selected_filesystem, 274 | }) 275 | 276 | return { status=200, reason="ok", body = body } 277 | end 278 | 279 | function writeZFS(request) 280 | local req = parseRequest(request.body) 281 | 282 | local disks = req.disk 283 | 284 | db.update("zfs_filesystem", req.filesystem) 285 | db.update("zfs_disks", disks) 286 | 287 | return { status=303, headers = {["Location"]="/"}, reason="ok", body = "TODO" } 288 | end 289 | 290 | function setLanguage(request) 291 | local req = parseRequest(request.body) 292 | 293 | db.update("installer_language", req.language) 294 | 295 | --TODO: set more defaults based on language selection 296 | local keymap_layout = lang.languages[req.language].keymap_layout 297 | local keymap_variant = lang.languages[req.language].keymap_variant 298 | 299 | local parsed_db = db.parse() 300 | 301 | if (not parsed_db.keymap_layout or parsed_db.keymap_layout == "") then 302 | keymap.setKeymap(keymap_layout, keymap_variant) 303 | db.update("keymap_layout", keymap_layout) 304 | db.update("keymap_variant", keymap_variant) 305 | end 306 | 307 | return { status=303, headers = {["Location"]="/"}, reason="ok", body = "TODO" } 308 | end 309 | 310 | function keymapPage(request) 311 | local parsed_db = db.parse() 312 | local selected_layout = parsed_db.keymap_layout 313 | local selected_variant = parsed_db.keymap_variant 314 | local variants = keymap.XMap[selected_layout] or {} 315 | local cur_lang = parsed_db.installer_language 316 | 317 | local body = template.process(keymap_page, 318 | { 319 | cur_lang = cur_lang, 320 | lang = lang.translations, 321 | selected_layout = selected_layout, 322 | selected_variant = selected_variant, 323 | x_list = keymap.XList, 324 | x_variants = variants, 325 | }) 326 | 327 | return { status=200, reason="ok", body = body } 328 | end 329 | 330 | function getVariants(request) 331 | local variants = keymap.XMap[request.matches[1]] 332 | return { status=200, reason="ok", body=json.encode(variants)} 333 | end 334 | 335 | function setKeymap(request) 336 | local map = json.decode(request.body) 337 | 338 | keymap.setKeymap(map.layout, map.variant) 339 | db.update("keymap_layout", map.layout) 340 | db.update("keymap_variant", map.variant) 341 | 342 | return { status=200, reason="ok", body = map.layout} 343 | end 344 | 345 | function addUserPage(request) 346 | local parsed_db = db.parse() 347 | local cur_lang = parsed_db.installer_language 348 | 349 | local body = template.process(add_user_page, 350 | { 351 | cur_lang = cur_lang, 352 | lang = lang.translations, 353 | operator = true, 354 | shells = shell.list, 355 | }) 356 | 357 | return { status=200, reason="ok", body = body } 358 | end 359 | 360 | function editUserPage(request) 361 | local parsed_db = db.parse() 362 | local cur_lang = parsed_db.installer_language 363 | 364 | local username = request.matches[1] 365 | 366 | local user_data = db.parse().users[username] 367 | 368 | local groups = splitString(user_data.groups, " ") 369 | local trimmed_groups = "" 370 | local wheel = false 371 | local operator = false 372 | 373 | for _, group in ipairs(groups) do 374 | if (group == "wheel") then 375 | wheel = true 376 | elseif (group == "operator") then 377 | operator = true 378 | else 379 | trimmed_groups = trimmed_groups..group.." " 380 | end 381 | end 382 | 383 | local body = template.process(add_user_page, 384 | { 385 | cur_lang = cur_lang, 386 | editing = true, 387 | full_name = user_data.full_name, 388 | groups = trimmed_groups, 389 | wheel = wheel, 390 | operator = operator, 391 | lang = lang.translations, 392 | shells = shell.list, 393 | username = username, 394 | user_shell = user_data.shell, 395 | }) 396 | 397 | return { status=200, reason="ok", body = body } 398 | end 399 | 400 | function addUser(request) 401 | local req = parseRequest(request.body) 402 | 403 | --TODO verify input 404 | 405 | local groups = req.groups; 406 | if (req.wheel) then 407 | groups = groups.." wheel" 408 | end 409 | if (req.operator) then 410 | groups = groups.." operator" 411 | end 412 | 413 | db.update("user:"..req.username..":full_name", req.full_name) 414 | db.update("user:"..req.username..":password", req.password) 415 | db.update("user:"..req.username..":groups", groups) 416 | db.update("user:"..req.username..":shell", req.shell) 417 | 418 | return { status=303, headers = {["Location"]="/#users"}, reason="ok", body = "TODO" } 419 | end 420 | 421 | function deleteUser(request) 422 | local username = request.matches[1] 423 | 424 | --TODO verify input 425 | 426 | db.removeMatches("^user:"..username..":") 427 | 428 | return { status=303, headers = {["Location"]="/#users"}, reason="ok", body = "TODO" } 429 | end 430 | 431 | function networkPage(request) 432 | local parsed_db = db.parse() 433 | local cur_lang = parsed_db.installer_language 434 | 435 | local body = template.process(network_page, 436 | { 437 | cur_lang = cur_lang, 438 | lang = lang.translations, 439 | network_interfaces = network.getInterfaces(), 440 | }) 441 | 442 | return { status=200, reason="ok", body = body } 443 | end 444 | 445 | function checkIsWireless(request) 446 | local isWireless = network.isWireless(request.matches[1]) 447 | return {status=200, reason="ok", body=json.encode(isWireless)} 448 | end 449 | 450 | function getNetworkStatus(request) 451 | local status = network.status_from_command() 452 | local status_from_file = network.status_from_file() 453 | return { status=200, reason="ok", body=status_from_file} 454 | end 455 | 456 | function scanWireless(request) 457 | local networks = network.scanWireless() 458 | return { status=200, reason="ok", body=json.encode(networks)} 459 | end 460 | 461 | function connectToNetwork(request) 462 | local req = json.decode(request.body) 463 | 464 | network.connectWireless(req.network, req.password) 465 | 466 | return { status=200, reason="ok", body = "okay"} 467 | end 468 | 469 | function writeNetwork(request) 470 | local req = json.decode(request.body) 471 | 472 | db.update("network_interface", req.network_interface) 473 | db.update("network", req.network) 474 | db.update("network_password", req.password) 475 | 476 | local resolv_file = io.open("/etc/resolv.conf", "r") 477 | while (resolv_file == nil) do --TODO: there must be a better way to do this 478 | os.execute("sleep 1") 479 | resolv_file = io.open("/etc/resolv.conf", "r") 480 | end 481 | 482 | for line in resolv_file:lines() do 483 | local key, val = line:match("^([^ ]+) (.*)") 484 | if (key == "search") then 485 | db.update("resolv_search", val) 486 | elseif (key == "nameserver") then 487 | db.update("resolv_nameserver", val) 488 | end 489 | end 490 | 491 | resolv_file:close() 492 | 493 | return { status=303, headers = {["Location"]="/"}, reason="ok", body = "TODO" } 494 | end 495 | 496 | function doInstall(request) 497 | local req = parseRequest(request.body) 498 | for key, val in pairs(req) do 499 | print(key, val) 500 | end 501 | db.update("root_password", req.root_password) 502 | 503 | --For testing, I don't run the actual install command. Check the 'liveuser' branch 504 | --install.install() 505 | --os.execute("cd "..SRC_DIR.." && sleep 1 && cat installscript && ./bsdinstall-scripted installscript") 506 | db.close() 507 | 508 | return { status=501, reason="Not implemented", body="TODO" } 509 | end 510 | 511 | local server = httpd.create_server("/var/log/httpd.log") 512 | 513 | server:add_route("GET", "^/style$", getStylesheet) 514 | 515 | --Fetch API stuff 516 | server:add_route("GET", "^/keymap/variants/(.*)$", getVariants) 517 | server:add_route("GET", "^/network/iswireless/(.*)$", checkIsWireless) 518 | server:add_route("GET", "^/scanwireless$", scanWireless) 519 | server:add_route("GET", "^/networkstatus$", getNetworkStatus) 520 | server:add_route("POST", "^/setkeymap$", setKeymap) 521 | server:add_route("POST", "^/network$", connectToNetwork) 522 | server:add_route("POST", "^/networkconfirm$", writeNetwork) 523 | server:add_route("POST", "^/pkgsets$", writePackages) 524 | server:add_route("POST", "^/hostname$", writeHostname) 525 | 526 | --Pages 527 | server:add_route("GET", "^/language$", languagePage) 528 | server:add_route("GET", "^/zfs$", zfsPage) 529 | server:add_route("GET", "^/adduser$", addUserPage) 530 | server:add_route("GET", "^/edituser/(.*)$", editUserPage) 531 | server:add_route("GET", "^/network$", networkPage) 532 | server:add_route("GET", "^/keymap$", keymapPage) 533 | server:add_route("GET", "^/$", mainPage) 534 | 535 | server:add_route("POST", "^/zfs$", writeZFS) 536 | server:add_route("POST", "^/setlanguage", setLanguage) 537 | server:add_route("POST", "^/install$", doInstall) 538 | server:add_route("POST", "^/adduser$", addUser) 539 | server:add_route("POST", "^/deleteuser/(.*)$", deleteUser) 540 | 541 | server:run(true) 542 | -------------------------------------------------------------------------------- /restapi: -------------------------------------------------------------------------------- 1 | #!/usr/libexec/flua 2 | -- 3 | -- Minimal web server written in Lua 4 | -- 5 | -- Use with inetd, no other dependencies: 6 | -- http stream tcp nowait www /usr/local/sbin/httpd httpd 7 | 8 | -- 9 | -- Copyright (c) 2016 Ryan Moeller 10 | -- 11 | -- Permission to use, copy, modify, and distribute this software for any 12 | -- purpose with or without fee is hereby granted, provided that the above 13 | -- copyright notice and this permission notice appear in all copies. 14 | -- 15 | -- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 16 | -- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 17 | -- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 18 | -- ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 19 | -- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 20 | -- ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 21 | -- OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 22 | -- 23 | 24 | -- Adjust the following paths as needed. 25 | server_log_path = "/var/log/httpd.log" 26 | demo_db_path = "/var/db/demo/demo.db" 27 | 28 | -- This is one way to set the paths for importing additional packages. 29 | package.cpath = "/usr/pkg/luarocks/lib/lua/5.3/?.so;" .. package.cpath 30 | package.path = "/usr/pkg/luarocks/share/lua/5.3/?.lua;" .. package.path 31 | package.path = "/usr/pkg/luarocks/share/lua/5.3/?/init.lua;" .. package.path 32 | 33 | -- Another way would be to set the LUA_PATH and LUA_CPATH environment 34 | -- variables in a wrapper shell script, such as: 35 | --[[ 36 | #! /bin/sh 37 | LUAROCKS="/usr/pkg/luarocks" 38 | LUA_PATH="$LUAROCKS/share/lua/5.3/?.lua;$LUAROCKS/share/lua/5.3/?/init.lua" 39 | LUA_CPATH="$LUAROCKS/lib/lua/5.3/?.so" 40 | export LUA_PATH 41 | export LUA_CPATH 42 | /path/to/this/script 43 | ]] 44 | -- Then the wrapper script can be invoked by inetd instead. 45 | 46 | -- luarocks install lua-cjson lsqlite3 47 | local cjson = require("cjson") 48 | local sqlite = require("lsqlite3") 49 | 50 | -- httpd.lua from this repo should be installed somewhere in package.path 51 | local httpd = require("httpd") 52 | 53 | -- Creating the server early allows us to use the log file in our handlers. 54 | local server = httpd.create_server(server_log_path) 55 | 56 | local db, err, msg = sqlite.open(demo_db_path) 57 | if not db then 58 | server.log:write("Error opening database: ", msg, "\n") 59 | os.exit(1) 60 | end 61 | if db:exec( 62 | "CREATE TABLE IF NOT EXISTS numbers(num INTEGER PRIMARY KEY)" 63 | ) ~= sqlite.OK then 64 | server.log:write("Error initializing database: ", db:errmsg(), "\n") 65 | os.exit(1) 66 | end 67 | 68 | function hello_world(request) 69 | return { 70 | status=200, reason="ok", 71 | headers={ ["Content-Type"]="application/json" }, 72 | body=cjson.encode({ message="Hello, world!" }) 73 | } 74 | end 75 | 76 | function numbers_list(request) 77 | local numbers = {} 78 | for num in db:urows("SELECT * FROM numbers") do 79 | table.insert(numbers, num) 80 | end 81 | return { 82 | status=200, reason="ok", 83 | headers={ ["Content-Type"]="application/json" }, 84 | body=cjson.encode(numbers) 85 | } 86 | end 87 | 88 | local NO_CONTENT = { 89 | status=204, reason="no content" 90 | } 91 | 92 | local BAD_REQUEST = { 93 | status=400, reason="bad request", 94 | headers={ ["Content-Type"]="application/json" }, 95 | body=cjson.encode("bad request") 96 | } 97 | 98 | local NOT_FOUND = { 99 | status=404, reason="not found", 100 | headers={ ["Content-Type"]="application/json" }, 101 | body=cjson.encode({ error="resource does not exist" }) 102 | } 103 | 104 | local CONFLICT = { 105 | status=409, reason="conflict", 106 | headers={ ["Content-Type"]="application/json" }, 107 | body=cjson.encode({ error="resource already exists" }) 108 | } 109 | 110 | local INTERNAL_SERVER_ERROR = { 111 | status=503, reason="error", 112 | headers={ ["Content-Type"]="application/json" }, 113 | body=cjson.encode({ error="internal server error" }) 114 | } 115 | 116 | function numbers_add(request) 117 | local stmt = db:prepare("INSERT INTO numbers(num) VALUES (?)") 118 | local num = tonumber(request.body) 119 | if not num then 120 | return BAD_REQUEST 121 | end 122 | if stmt:bind_values(num) ~= sqlite.OK then 123 | server.log:write("Error binding parameter: ", db:errmsg(), "\n") 124 | return INTERNAL_SERVER_ERROR 125 | end 126 | local res = stmt:step() 127 | if res == sqlite.DONE then 128 | local location = "/numbers/" .. num 129 | return { 130 | status=201, reason="created", 131 | headers={ ["Location"]=location } 132 | } 133 | elseif res == sqlite.CONSTRAINT and string.find(db:errmsg(), "UNIQUE") then 134 | return CONFLICT 135 | else 136 | server.log:write("Error executing statement: ", db:errmsg(), "\n") 137 | return INTERNAL_SERVER_ERROR 138 | end 139 | end 140 | 141 | function numbers_get(request) 142 | local stmt = db:prepare("SELECT * FROM numbers WHERE num = ?") 143 | local num = tonumber(request.matches[1]) 144 | if stmt:bind_values(num) ~= sqlite.OK then 145 | server.log:write("Error binding parameter: ", db:errmsg(), "\n") 146 | return INTERNAL_SERVER_ERROR 147 | end 148 | if stmt:step() == sqlite.ROW then 149 | return { 150 | status=200, reason="ok", 151 | headers={ ["Content-Type"]="application/json" }, 152 | body=cjson.encode(num) 153 | } 154 | else 155 | return NOT_FOUND 156 | end 157 | end 158 | 159 | function numbers_delete(request) 160 | local stmt = db:prepare("DELETE FROM numbers WHERE num = ?") 161 | local num = tonumber(request.matches[1]) 162 | if stmt:bind_values(num) ~= sqlite.OK then 163 | server.log:write("Error binding parameter: ", db:errmsg(), "\n") 164 | return INTERNAL_SERVER_ERROR 165 | end 166 | if stmt:step() ~= sqlite.DONE then 167 | server.log:write("Error executing statement: ", db:errmsg(), "\n") 168 | return INTERNAL_SERVER_ERROR 169 | end 170 | return NO_CONTENT 171 | end 172 | 173 | function METHOD_NOT_ALLOWED(allowed) 174 | return { 175 | status=405, reason="method not allowed", 176 | headers={ ["Content-Type"]="application/json", 177 | ["Allow"]=table.concat(allowed, ",") }, 178 | body=cjson.encode({ error="method not allowed" }) 179 | } 180 | end 181 | 182 | function method_not_allowed(allowed) 183 | return function(request) 184 | return METHOD_NOT_ALLOWED(allowed) 185 | end 186 | end 187 | 188 | function not_found(request) 189 | return NOT_FOUND 190 | end 191 | 192 | server:add_route("GET", "^/$", hello_world) 193 | for _,m in ipairs({ 194 | "PUT","POST","PATCH","DELETE","HEAD","TRACE","OPTIONS","CONNECT" 195 | }) do 196 | server:add_route(m, "^/$", method_not_allowed{"GET"}) 197 | end 198 | 199 | server:add_route("GET", "^/numbers$", numbers_list) 200 | server:add_route("POST", "^/numbers$", numbers_add) 201 | for _,m in ipairs({ 202 | "PUT","PATCH","DELETE","HEAD","TRACE","OPTIONS","CONNECT" 203 | }) do 204 | server:add_route(m, "^/numbers$", method_not_allowed{"GET","POST"}) 205 | end 206 | 207 | server:add_route("GET", "^/numbers/(%d+)$", numbers_get) 208 | server:add_route("DELETE", "^/numbers/(%d+)$", numbers_delete) 209 | for _,m in ipairs({ 210 | "PUT","POST","PATCH","HEAD","TRACE","OPTIONS","CONNECT" 211 | }) do 212 | server:add_route(m, "^/numbers/(%d+)$", method_not_allowed{"GET","DELETE"}) 213 | end 214 | 215 | for _,m in ipairs({ 216 | "GET","PUT","POST","PATCH","DELETE","HEAD","TRACE","OPTIONS","CONNECT" 217 | }) do 218 | server:add_route(m, ".*", not_found) 219 | end 220 | 221 | server:run(true) 222 | -------------------------------------------------------------------------------- /screenshots/add-user-2021-08-04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yangzhong-freebsd/lua-httpd/b2fa17fdce00b613b4b9f0a853aefaa5f9470395/screenshots/add-user-2021-08-04.png -------------------------------------------------------------------------------- /screenshots/main-page-2021-08-04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yangzhong-freebsd/lua-httpd/b2fa17fdce00b613b4b9f0a853aefaa5f9470395/screenshots/main-page-2021-08-04.png -------------------------------------------------------------------------------- /server/boot.lua: -------------------------------------------------------------------------------- 1 | -- vim: set et sw=4: 2 | -- 3 | -- Copyright (c) 2020 Ryan Moeller 4 | -- 5 | -- Permission to use, copy, modify, and distribute this software for any 6 | -- purpose with or without fee is hereby granted, provided that the above 7 | -- copyright notice and this permission notice appear in all copies. 8 | -- 9 | -- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | -- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | -- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | -- ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | -- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | -- ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | -- OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -- 17 | 18 | local boot = boot or {} 19 | 20 | function boot.howto() 21 | local f = io.popen("sysctl -n debug.boothowto", "r") 22 | local text = f:read("*a") 23 | f:close() 24 | local howto = tonumber(text) 25 | return { 26 | autoboot = howto == 0, 27 | askname = howto & 0x00000001 ~= 0, 28 | single = howto & 0x00000002 ~= 0, 29 | nosync = howto & 0x00000004 ~= 0, 30 | halt = howto & 0x00000008 ~= 0, 31 | initname = howto & 0x00000010 ~= 0, 32 | dfltroot = howto & 0x00000020 ~= 0, 33 | kdb = howto & 0x00000040 ~= 0, 34 | rdonly = howto & 0x00000080 ~= 0, 35 | dump = howto & 0x00000100 ~= 0, 36 | miniroot = howto & 0x00000200 ~= 0, 37 | -- mysterious empty slot -- 38 | verbose = howto & 0x00000800 ~= 0, 39 | serial = howto & 0x00001000 ~= 0, 40 | cdrom = howto & 0x00002000 ~= 0, 41 | poweroff = howto & 0x00004000 ~= 0, 42 | gdb = howto & 0x00008000 ~= 0, 43 | mute = howto & 0x00010000 ~= 0, 44 | selftest = howto & 0x00020000 ~= 0, 45 | reserved1 = howto & 0x00040000 ~= 0, 46 | reserved2 = howto & 0x00080000 ~= 0, 47 | pause = howto & 0x00100000 ~= 0, 48 | reroot = howto & 0x00200000 ~= 0, 49 | powercycle = howto & 0x00400000 ~= 0, 50 | -- more empty slots -- 51 | probe = howto & 0x10000000 ~= 0, 52 | multiple = howto & 0x20000000 ~= 0, 53 | bootinfo = howto & 0x40000000 ~= 0, 54 | } 55 | end 56 | 57 | function boot.method() 58 | local f = io.popen("sysctl -n machdep.bootmethod", "r") 59 | local text = f:read("*a") 60 | f:close() 61 | local bootmethod = text:match("([^\n]+)") 62 | return bootmethod 63 | end 64 | 65 | return boot 66 | -------------------------------------------------------------------------------- /server/db.lua: -------------------------------------------------------------------------------- 1 | -- vim: set et: 2 | -- Minimal web server written in Lua 3 | -- 4 | -- Use with inetd, no other dependencies: 5 | -- http stream tcp nowait root /usr/local/sbin/httpd httpd 6 | 7 | -- 8 | -- Copyright (c) 2016 - 2020 Ryan Moeller 9 | -- 10 | -- Permission to use, copy, modify, and distribute this software for any 11 | -- purpose with or without fee is hereby granted, provided that the above 12 | -- copyright notice and this permission notice appear in all copies. 13 | -- 14 | -- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 15 | -- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 16 | -- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 17 | -- ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 18 | -- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 19 | -- ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 20 | -- OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 21 | 22 | local db = {} 23 | 24 | local misc = require("misc") 25 | 26 | local db_filename = SRC_DIR .. "db" 27 | local db_file = io.open(db_filename, "r+") 28 | 29 | function db.close() 30 | db_file:close() 31 | end 32 | 33 | function db.parse() 34 | local table = {users = {}} 35 | 36 | db_file:seek("set") 37 | local contents = db_file:read("*all") 38 | local lines = misc.splitString(contents, "\n") 39 | for i, line in ipairs(lines) do 40 | if (line:match("^user:")) then 41 | local username, prop, val = line:match("^[^:]+:([^:]+):([^=]+)=(.*)") 42 | if (table.users[username] == nil) then 43 | table.users[username] = {} 44 | end 45 | table.users[username][prop] = val 46 | else 47 | local prop, val = line:match("^([^=]+)=(.*)") 48 | table[prop] = val 49 | end 50 | end 51 | 52 | return table 53 | end 54 | 55 | --Writes key=val to the end of the db. 56 | --If key already exists in the db, we delete that line first. 57 | function db.update(key, val) 58 | db.removeMatches("^"..key.."$") 59 | db_file:seek("end") 60 | db_file:write(key.."="..val.."\n") 61 | end 62 | 63 | 64 | --TODO maybe make this faster? it's looping through db twice currently 65 | --Only writes key=val to db if either key doesn't exist, or 66 | --if val is "". 67 | function db.updateIfUnset(key, val) 68 | local parsed_db = db.parse() 69 | if (not parsed_db[key] or parsed_db[key] == "") then 70 | db.update(key, val) 71 | end 72 | end 73 | function db.getUsersAsList(parsed_db) 74 | local users = parsed_db.users 75 | local user_list = {} 76 | 77 | for username, props in pairs(users) do 78 | local user = {} 79 | user.username = username 80 | for key, val in pairs(props) do 81 | user[key] = val 82 | end 83 | table.insert(user_list, user) 84 | end 85 | 86 | local function compare_users(this, other) 87 | return this.username < other.username 88 | end 89 | 90 | table.sort(user_list, compare_users) 91 | 92 | return user_list 93 | end 94 | 95 | --Pass in a regex for the key, and this function will 96 | --remove all lines in the db with matching keys. 97 | function db.removeMatches(regex) 98 | db_file:seek("set") 99 | local text = db_file:read("*all") 100 | 101 | db_file:close() 102 | db_file = io.open(db_filename, "w+") 103 | 104 | local split_list = misc.splitString(text, "\n") 105 | for _, line in ipairs(split_list) do 106 | local key = line:match("^([^=]+)") 107 | if (not key:match(regex)) then 108 | db_file:write(line.."\n") 109 | end 110 | end 111 | end 112 | 113 | return db 114 | -------------------------------------------------------------------------------- /server/disk.lua: -------------------------------------------------------------------------------- 1 | -- vim: set et sw=4: 2 | -- 3 | -- Copyright (c) 2020 Ryan Moeller 4 | -- 5 | -- Permission to use, copy, modify, and distribute this software for any 6 | -- purpose with or without fee is hereby granted, provided that the above 7 | -- copyright notice and this permission notice appear in all copies. 8 | -- 9 | -- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | -- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | -- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | -- ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | -- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | -- ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | -- OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -- 17 | 18 | local disk = disk or {} 19 | 20 | local function kern_disks() 21 | local f = io.popen("sysctl -n kern.disks | xargs -n1;" .. 22 | "ggatel list;" .. 23 | "mdconfig -l | xargs -n1;", 24 | "r") 25 | local text = f:read("*a") 26 | f:close() 27 | return text 28 | end 29 | 30 | local function diskinfo(dev) 31 | local f = io.popen("diskinfo -v "..dev .. " 2>/dev/null", "r") 32 | local text = f:read("*a") 33 | f:close() 34 | return text 35 | end 36 | 37 | function disk.info() 38 | local disks = {} 39 | 40 | for dev in kern_disks():gmatch("([^ \n]+)") do 41 | local disk = {} 42 | 43 | for line in diskinfo(dev):gmatch("([^\n]+)") do 44 | if line:find("#") then 45 | local value, field = line:match("^\t([^\t]+)\t+# (.*)$") 46 | 47 | local size_human = field:match("mediasize in bytes %((.*)%)") 48 | if size_human then 49 | disk["mediasize in bytes"] = value 50 | disk["mediasize in bytes human"] = size_human 51 | else 52 | disk[field] = value 53 | end 54 | end 55 | end 56 | 57 | if (disk["mediasize in bytes"]) then 58 | disks[dev] = disk 59 | end 60 | end 61 | return disks 62 | end 63 | 64 | --TODO: make this use the real disk names 65 | function disk.prettyPrintDisks(disk_str) 66 | return disk_str 67 | end 68 | 69 | return disk 70 | -------------------------------------------------------------------------------- /server/filesystem.lua: -------------------------------------------------------------------------------- 1 | -- vim: set et sw=4: 2 | -- 3 | -- Copyright (c) 2020 Ryan Moeller 4 | -- 5 | -- Permission to use, copy, modify, and distribute this software for any 6 | -- purpose with or without fee is hereby granted, provided that the above 7 | -- copyright notice and this permission notice appear in all copies. 8 | -- 9 | -- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | -- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | -- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | -- ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | -- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | -- ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | -- OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -- 17 | 18 | local filesystem = filesystem or {} 19 | 20 | -- TODO: minimum requirements (in js) 21 | filesystem.formats = { 22 | { 23 | title = "UFS (concat)", 24 | value = "UFS:CONCAT", 25 | default = false, 26 | }, 27 | { 28 | title = "UFS (mirror)", 29 | value = "UFS:MIRROR", 30 | default = false, 31 | }, 32 | { 33 | title = "UFS (raid5)", 34 | value = "UFS:RAID5", 35 | default = false, 36 | }, 37 | { 38 | title = "UFS (stripe)", 39 | value = "UFS:STRIPE", 40 | default = false, 41 | }, 42 | { 43 | title = "ZFS (mirror)", 44 | value = "ZFS:MIRROR", 45 | default = true, 46 | }, 47 | { 48 | title = "ZFS (raidz1)", 49 | value = "ZFS:RAIDZ1", 50 | default = false, 51 | }, 52 | { 53 | title = "ZFS (raidz2)", 54 | value = "ZFS:RAIDZ2", 55 | default = false, 56 | }, 57 | { 58 | title = "ZFS (stripe)", 59 | value = "ZFS:STRIPE", 60 | default = false, 61 | }, 62 | } 63 | 64 | filesystem.zfs_formats = { 65 | { 66 | title = "Stripe", 67 | value = "stripe", 68 | min_disks = 1, 69 | }, 70 | { 71 | title = "Mirror", 72 | value = "mirror", 73 | min_disks = 2, 74 | }, 75 | { 76 | title = "RAID-Z1", 77 | value = "raidz1", 78 | min_disks = 3, 79 | }, 80 | { 81 | title = "RAID-Z2", 82 | value = "raidz2", 83 | min_disks = 4, 84 | }, 85 | } 86 | 87 | return filesystem 88 | -------------------------------------------------------------------------------- /server/hardening.lua: -------------------------------------------------------------------------------- 1 | -- vim: set et sw=4: 2 | -- 3 | -- Copyright (c) 2020 Ryan Moeller 4 | -- 5 | -- Permission to use, copy, modify, and distribute this software for any 6 | -- purpose with or without fee is hereby granted, provided that the above 7 | -- copyright notice and this permission notice appear in all copies. 8 | -- 9 | -- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | -- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | -- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | -- ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | -- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | -- ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | -- OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -- 17 | 18 | local hardening = hardening or {} 19 | 20 | hardening.menu = { 21 | { 22 | name = "hide_uids", 23 | description = "Hide processes running as other users", 24 | }, 25 | { 26 | name = "hide_gids", 27 | description = "Hide processes running as other groups", 28 | }, 29 | { 30 | name = "hide_jail", 31 | description = "Hide processes running in jails", 32 | }, 33 | { 34 | name = "read_msgbuf", 35 | description = 36 | "Disable reading kernel message buffer for unprivileged users", 37 | }, 38 | { 39 | name = "proc_debug", 40 | description = 41 | "Disable process debugging facilities for unprivileged users", 42 | }, 43 | { 44 | name = "random_pid", 45 | description = "Randomize the PID of newly created processes", 46 | }, 47 | { 48 | name = "clear_tmp", 49 | description = "Clean the /tmp filesystem on system startup", 50 | }, 51 | { 52 | name = "disable_syslogd", 53 | description = 54 | "Disable opening Syslogd network socket (disables remote logging)", 55 | }, 56 | { 57 | name = "disable_sendmail", 58 | description = "Disable Sendmail service", 59 | }, 60 | { 61 | name = "secure_console", 62 | description = "Enable console password prompt", 63 | }, 64 | { 65 | name = "disable_ddtrace", 66 | description = "Disallow DTrace destructive-mode", 67 | }, 68 | } 69 | 70 | return hardening 71 | -------------------------------------------------------------------------------- /server/httpd.lua: -------------------------------------------------------------------------------- 1 | -- vim: set et sw=4: 2 | -- Minimal web server written in Lua 3 | -- 4 | -- Use with inetd, no other dependencies: 5 | -- http stream tcp nowait www /usr/local/sbin/httpd httpd 6 | 7 | -- 8 | -- Copyright (c) 2016 - 2020 Ryan Moeller 9 | -- 10 | -- Permission to use, copy, modify, and distribute this software for any 11 | -- purpose with or without fee is hereby granted, provided that the above 12 | -- copyright notice and this permission notice appear in all copies. 13 | -- 14 | -- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 15 | -- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 16 | -- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 17 | -- ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 18 | -- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 19 | -- ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 20 | -- OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 21 | -- 22 | 23 | local M = {} 24 | 25 | M.VERSION = '0.0.1' 26 | 27 | 28 | -- HTTP-message = start-line 29 | -- *( header-field CRLF ) 30 | -- CRLF 31 | -- [ message-body ] 32 | -- 33 | -- The server reads HTTP-messages from stdin and parses the data in order 34 | -- to route and dispatch to a handler for processing. 35 | -- 36 | -- The server state is a waiting state. So if server.state == START_LINE, 37 | -- we're looking for a start line next. 38 | local ServerState = { 39 | START_LINE = 0, 40 | HEADER_FIELD = 1 41 | } 42 | 43 | 44 | -- start-line = request-line / status-line 45 | -- request-line = method SP request-target SP HTTP-version CRLF 46 | -- method = token 47 | local function parse_start_line(method, line) 48 | local pattern = "^" .. method .. " (%g+) (HTTP/1.1)\r$" 49 | 50 | return string.match(line, pattern) 51 | end 52 | 53 | 54 | local function decode(s) 55 | local function char(hex) 56 | return string.char(tonumber(hex, 16)) 57 | end 58 | 59 | s = string.gsub(s, "+", " ") 60 | s = string.gsub(s, "%%(%x%x)", char) 61 | s = string.gsub(s, "\r\n", "\n") 62 | 63 | return s 64 | end 65 | 66 | 67 | --[[ 68 | local function encode(s) 69 | local function hex(char) 70 | return string.format("%%%02X", string.byte(char)) 71 | end 72 | 73 | s = string.gsub(s, "\n", "\r\n") 74 | s = string.gsub(s, "([^%w %-%_%.%~])", hex) 75 | s = string.gsub(s, " ", "+") 76 | return s 77 | end 78 | ]]-- 79 | 80 | 81 | local function parse_request_query(query) 82 | local params = {} 83 | 84 | local function parse(kv) 85 | local encoded_key, encoded_value = string.match(kv, "^(.*)=(.*)$") 86 | if encoded_key ~= nil then 87 | local key = decode(encoded_key) 88 | local value = decode(encoded_value) 89 | local param = params[key] or {} 90 | table.insert(param, value) 91 | params[key] = param 92 | end 93 | end 94 | 95 | string.gsub(query, "([^;&]+)", parse) 96 | 97 | return params 98 | end 99 | 100 | 101 | local function parse_request_path(s) 102 | local encoded_path, encoded_query = string.match(s, "^(.*)%?(.*)$") 103 | if encoded_path == nil then 104 | return decode(s), {} 105 | end 106 | 107 | local path = decode(encoded_path) 108 | local params = parse_request_query(encoded_query) 109 | 110 | return path, params 111 | end 112 | 113 | 114 | local methods = { 115 | "GET", "HEAD", "POST", "PUT", "DELETE", "CONNECT", "OPTIONS", "TRACE" 116 | } 117 | 118 | local function handle_start_line(server, line) 119 | -- Try to match each known method. 120 | for _, method in ipairs(methods) do 121 | local rawpath, version = parse_start_line(method, line) 122 | 123 | if rawpath ~= nil then 124 | local path, params = parse_request_path(rawpath) 125 | server.request = { 126 | method = method, 127 | path = path, 128 | params = params, 129 | version = version, 130 | headers = {}, 131 | cookies = {} 132 | } 133 | 134 | return ServerState.HEADER_FIELD 135 | end 136 | end 137 | 138 | -- No known methods matched. 139 | server.log:write("Invalid start-line in request.\n") 140 | 141 | return ServerState.START_LINE 142 | end 143 | 144 | 145 | -- expects a server table and a response table 146 | -- example response table: 147 | -- { status=404, reason="not found", headers={}, cookies={}, 148 | -- body="404 Not Found" } 149 | local function write_http_response(server, response) 150 | local output = server.output 151 | 152 | local status = response.status 153 | local reason = response.reason 154 | local headers = response.headers or {} 155 | local cookies = response.cookies or {} 156 | local body = response.body 157 | 158 | if type(body) == "string" then 159 | headers['Content-Length'] = #body 160 | end 161 | 162 | local statusline = string.format("HTTP/1.1 %03d %s\r\n", status, reason) 163 | output:write(statusline) 164 | 165 | for name, value in pairs(headers) do 166 | local header = string.format("%s: %s\r\n", name, value) 167 | output:write(header) 168 | end 169 | 170 | for name, value in pairs(cookies) do 171 | local cookie = string.format("Set-Cookie: %s=%s\r\n", name, value) 172 | output:write(cookie) 173 | end 174 | 175 | output:write("\r\n") 176 | 177 | if type(body) == "string" then 178 | output:write(body) 179 | elseif type(body) == "function" then 180 | output:flush() 181 | body(output) 182 | end 183 | end 184 | 185 | 186 | --[[ 187 | -- Log some debugging info 188 | local function debug_server(server) 189 | local log = server.log 190 | local request = server.request 191 | local handlers = server.handlers[request.method] 192 | 193 | log:write("#server.handlers = " .. tostring(#server.handlers) .. "\n") 194 | log:write("#handlers = " .. tostring(#handlers) .. "\n") 195 | log:write(request.method .. "\n") 196 | if request.path ~= nil then 197 | log:write(request.path, "\n") 198 | end 199 | for k, v in pairs(request.headers) do 200 | log:write("> " .. k .. ": ") 201 | for k1, v1 in pairs(v) do 202 | log:write(k1, "->") 203 | for _, v2 in ipairs(v1) do 204 | log:write(v2 .. ", ") 205 | end 206 | log:write("; ") 207 | end 208 | log:write("\n") 209 | end 210 | for k, v in pairs(request.params) do 211 | log:write(k .. " = ") 212 | for _, v in ipairs(v) do 213 | log:write(v .. ", ") 214 | end 215 | log:write("\n") 216 | end 217 | end 218 | ]]-- 219 | 220 | 221 | local function handle_request(server) 222 | local request = server.request 223 | local handlers = server.handlers[request.method] 224 | 225 | --debug_server(server) 226 | 227 | -- Try to service the request. 228 | local response = { status=404, reason="Not Found", body="not found" } 229 | for _, location in ipairs(handlers) do 230 | local pattern, handler = table.unpack(location) 231 | local matches = { string.match(request.path, pattern) } 232 | if #matches > 0 then 233 | request.matches = matches 234 | response = handler(request) 235 | break 236 | end 237 | end 238 | 239 | write_http_response(server, response) 240 | 241 | -- Close all open file handles and exit to complete the response. 242 | os.exit() 243 | end 244 | 245 | 246 | local function method_expects_body(method) 247 | return method == "POST" or method == "PUT" 248 | end 249 | 250 | 251 | local function handle_message_body(server, content_length) 252 | local body = "" 253 | repeat 254 | local buf = server.input:read(content_length - #body) 255 | if buf then 256 | body = body .. buf 257 | else 258 | server.log:write("body shorter than specified content length\n") 259 | break 260 | end 261 | until #body == content_length 262 | 263 | if server.verbose then 264 | server.log:write("content length = ", content_length, "\n") 265 | server.log:write(body, "\n") 266 | end 267 | server.request.body = body 268 | handle_request(server) 269 | end 270 | 271 | 272 | local function handle_blank_line(server) 273 | local request = server.request 274 | local method = request.method 275 | local content_length_header = request.headers['content-length'] 276 | 277 | if method_expects_body(method) and content_length_header then 278 | local values = content_length_header.list 279 | local value = values[#values] 280 | local content_length = tonumber(value) 281 | if content_length then 282 | return handle_message_body(server, content_length) 283 | else 284 | server.log:write("invalid content-length\n") 285 | end 286 | end 287 | return handle_request(server) 288 | end 289 | 290 | 291 | local function set_cookie(server, cookie) 292 | -- Browsers do not send cookie attributes in requests. 293 | local name, value = string.match(cookie, "(.+)=(.*)") 294 | local cookie = server.request.cookies[name] or {} 295 | table.insert(cookie, value) 296 | server.request.cookies[name] = cookie 297 | end 298 | 299 | 300 | local function parse_header_value(header, value) 301 | local function parse(attrib) 302 | local key, value = string.match(attrib, "^%s*(.*)=(.*)%s*$") 303 | if key then 304 | local attrval = header.dict[key] or {} 305 | table.insert(attrval, value) 306 | header.dict[key] = attrval 307 | else 308 | table.insert(header.list, attrib) 309 | end 310 | end 311 | 312 | string.gsub(value, "([^;]+)", parse) 313 | 314 | return header 315 | end 316 | 317 | 318 | local function update_header(server, name, value) 319 | local headers = server.request.headers 320 | -- Header may be repeated to form a list. 321 | local header = headers[name] or { dict={}, list={} } 322 | headers[name] = parse_header_value(header, value) 323 | end 324 | 325 | 326 | local function parse_header_field(line) 327 | return string.match(line, "^(%g+):%s*(.*)%s*\r$") 328 | end 329 | 330 | 331 | local function handle_header_field(server, line) 332 | if line == "\r" then 333 | -- When there are no headers left we get just a blank line. 334 | return handle_blank_line(server) 335 | else 336 | local name, value = parse_header_field(line) 337 | 338 | if name then 339 | -- Header field names are case-insensitive. 340 | local lname = string.lower(name) 341 | if lname == "cookie" then 342 | set_cookie(server, value) 343 | else 344 | update_header(server, lname, value) 345 | end 346 | else 347 | server.log:write("Ignoring invalid header: ", line, "\n") 348 | end 349 | 350 | -- Look for more headers. 351 | return ServerState.HEADER_FIELD 352 | end 353 | end 354 | 355 | 356 | local function handle_request_line(server, line) 357 | local state = server.state 358 | 359 | if state == ServerState.START_LINE then 360 | return handle_start_line(server, line) 361 | 362 | elseif state == ServerState.HEADER_FIELD then 363 | return handle_header_field(server, line) 364 | 365 | else 366 | return ServerState.START_LINE 367 | 368 | end 369 | end 370 | 371 | 372 | function M.create_server(logfile) 373 | local server = { 374 | state = ServerState.START_LINE, 375 | log = io.open(logfile, "a"), 376 | input = io.input(), 377 | output = io.output(), 378 | handlers = { 379 | -- handlers is a map of method => { location, location, ... } 380 | -- locations are matched in the order given, first match wins 381 | -- a location is an ordered list of { pattern, handler } 382 | -- pattern is a Lua pattern for string matching the path 383 | -- handler is a function(request) returning a response table 384 | GET = {}, 385 | HEAD = {}, 386 | POST = {}, 387 | PATCH = {}, 388 | PUT = {}, 389 | DELETE = {}, 390 | CONNECT = {}, 391 | OPTIONS = {}, 392 | TRACE = {} 393 | } 394 | } 395 | server.log:setvbuf("no") 396 | 397 | function server:add_route(method, pattern, handler) 398 | table.insert(self.handlers[method], { pattern, handler }) 399 | end 400 | 401 | function server:run(verbose) 402 | self.verbose = verbose 403 | for line in self.input:lines() do 404 | if verbose then 405 | self.log:write(line, "\n") 406 | end 407 | self.state = handle_request_line(self, line) 408 | end 409 | end 410 | 411 | return server 412 | end 413 | 414 | M.parse_query_string = parse_request_query 415 | 416 | return M 417 | -------------------------------------------------------------------------------- /server/install.lua: -------------------------------------------------------------------------------- 1 | 2 | local install = install or {} 3 | 4 | local db = require("db") 5 | 6 | function make_write(line, file) 7 | return "echo '"..line.."' >> "..file.."\n" 8 | end 9 | 10 | function install.install() 11 | local parsed_db = db.parse() 12 | 13 | local outfile = assert(io.open(SRC_DIR.."installscript", "w")) 14 | local users = db.getUsersAsList(parsed_db) 15 | 16 | outfile:write("PACKAGES=\"kernel base\"\n") 17 | outfile:write("OTHER_PACKAGES=\"vim-console sudo git\"\n") 18 | outfile:write("export ZFSBOOT_VDEV_TYPE="..parsed_db.zfs_filesystem.."\n") 19 | outfile:write("export ZFSBOOT_DISKS=\""..parsed_db.zfs_disks.."\"\n") 20 | outfile:write("export nonInteractive=\"YES\"\n") 21 | outfile:write("\n") 22 | outfile:write("#!/bin/sh\n") 23 | 24 | --TODO for all user-input parameters, need to escape any single quotes inside. Also change to use make_write 25 | outfile:write("echo 'hostname="..parsed_db.hostname.."' >> /etc/rc.conf\n") 26 | outfile:write("echo 'zfs_enable=\"YES\"' >> /etc/rc.conf\n") --TODO: check that this is really necessary 27 | outfile:write("echo '"..parsed_db.root_password.."' | pw usermod root -h 0\n") 28 | 29 | for _, user in ipairs(users) do 30 | local groups = user.groups:gsub(" ", ",") 31 | outfile:write("echo '"..user.password.."' | pw useradd -n "..user.username.." -c '"..user.full_name.."' -s "..user.shell.." -G "..groups.." -m -h 0\n") 32 | end 33 | 34 | --TODO this should only run when we're configuring a wireless network 35 | outfile:write(make_write("ctrl_interface=/var/run/wpa_supplicant", "/etc/wpa_supplicant.conf")) 36 | outfile:write(make_write("eapol_version=2", "/etc/wpa_supplicant.conf")) 37 | outfile:write(make_write("ap_scan=1", "/etc/wpa_supplicant.conf")) 38 | outfile:write(make_write("fast_reauth=1", "/etc/wpa_supplicant.conf")) 39 | 40 | outfile:write(make_write("network={", "/etc/wpa_supplicant.conf")) 41 | outfile:write(make_write(" ssid=\""..parsed_db.network.."\"", "/etc/wpa_supplicant.conf")) 42 | outfile:write(make_write(" scan_ssid=0", "/etc/wpa_supplicant.conf")) 43 | outfile:write(make_write(" psk=\""..parsed_db.network_password.."\"", "/etc/wpa_supplicant.conf")) 44 | outfile:write(make_write(" priority=5", "/etc/wpa_supplicant.conf")) 45 | outfile:write(make_write("}", "/etc/wpa_supplicant.conf")) 46 | 47 | outfile:write(make_write("wlans_"..parsed_db.network_interface.."=\"wlan0\"", "/etc/rc.conf")) 48 | outfile:write(make_write("ifconfig_wlan0=\"up scan WPA DHCP\"", "/etc/rc.conf")) 49 | outfile:write(make_write("ifconfig_wlan0_ipv6=\"inet6 accept_rtadv\"", "/etc/rc.conf")) 50 | outfile:write(make_write("create_args_wlan0=\"country CA regdomain FCC\"", "/etc/rc.conf")) 51 | 52 | outfile:write(make_write("search "..parsed_db.resolv_search, "/etc/resolv.conf")) 53 | outfile:write(make_write("nameserver "..parsed_db.resolv_nameserver, "/etc/resolv.conf")) 54 | 55 | outfile:close() 56 | end 57 | 58 | return install 59 | -------------------------------------------------------------------------------- /server/json.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- json.lua 3 | -- 4 | -- Copyright (c) 2020 rxi 5 | -- 6 | -- Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | -- this software and associated documentation files (the "Software"), to deal in 8 | -- the Software without restriction, including without limitation the rights to 9 | -- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 10 | -- of the Software, and to permit persons to whom the Software is furnished to do 11 | -- so, subject to the following conditions: 12 | -- 13 | -- The above copyright notice and this permission notice shall be included in all 14 | -- copies or substantial portions of the Software. 15 | -- 16 | -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | -- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | -- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | -- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | -- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | -- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | -- SOFTWARE. 23 | -- 24 | 25 | local json = { _version = "0.1.2" } 26 | 27 | ------------------------------------------------------------------------------- 28 | -- Encode 29 | ------------------------------------------------------------------------------- 30 | 31 | local encode 32 | 33 | local escape_char_map = { 34 | [ "\\" ] = "\\", 35 | [ "\"" ] = "\"", 36 | [ "\b" ] = "b", 37 | [ "\f" ] = "f", 38 | [ "\n" ] = "n", 39 | [ "\r" ] = "r", 40 | [ "\t" ] = "t", 41 | } 42 | 43 | local escape_char_map_inv = { [ "/" ] = "/" } 44 | for k, v in pairs(escape_char_map) do 45 | escape_char_map_inv[v] = k 46 | end 47 | 48 | 49 | local function escape_char(c) 50 | return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte())) 51 | end 52 | 53 | 54 | local function encode_nil(val) 55 | return "null" 56 | end 57 | 58 | 59 | local function encode_table(val, stack) 60 | local res = {} 61 | stack = stack or {} 62 | 63 | -- Circular reference? 64 | if stack[val] then error("circular reference") end 65 | 66 | stack[val] = true 67 | 68 | if rawget(val, 1) ~= nil or next(val) == nil then 69 | -- Treat as array -- check keys are valid and it is not sparse 70 | local n = 0 71 | for k in pairs(val) do 72 | if type(k) ~= "number" then 73 | error("invalid table: mixed or invalid key types") 74 | end 75 | n = n + 1 76 | end 77 | if n ~= #val then 78 | error("invalid table: sparse array") 79 | end 80 | -- Encode 81 | for i, v in ipairs(val) do 82 | table.insert(res, encode(v, stack)) 83 | end 84 | stack[val] = nil 85 | return "[" .. table.concat(res, ",") .. "]" 86 | 87 | else 88 | -- Treat as an object 89 | for k, v in pairs(val) do 90 | if type(k) ~= "string" then 91 | error("invalid table: mixed or invalid key types") 92 | end 93 | table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) 94 | end 95 | stack[val] = nil 96 | return "{" .. table.concat(res, ",") .. "}" 97 | end 98 | end 99 | 100 | 101 | local function encode_string(val) 102 | return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' 103 | end 104 | 105 | 106 | local function encode_number(val) 107 | -- Check for NaN, -inf and inf 108 | if val ~= val or val <= -math.huge or val >= math.huge then 109 | error("unexpected number value '" .. tostring(val) .. "'") 110 | end 111 | return string.format("%.14g", val) 112 | end 113 | 114 | 115 | local type_func_map = { 116 | [ "nil" ] = encode_nil, 117 | [ "table" ] = encode_table, 118 | [ "string" ] = encode_string, 119 | [ "number" ] = encode_number, 120 | [ "boolean" ] = tostring, 121 | } 122 | 123 | 124 | encode = function(val, stack) 125 | local t = type(val) 126 | local f = type_func_map[t] 127 | if f then 128 | return f(val, stack) 129 | end 130 | error("unexpected type '" .. t .. "'") 131 | end 132 | 133 | 134 | function json.encode(val) 135 | return ( encode(val) ) 136 | end 137 | 138 | 139 | ------------------------------------------------------------------------------- 140 | -- Decode 141 | ------------------------------------------------------------------------------- 142 | 143 | local parse 144 | 145 | local function create_set(...) 146 | local res = {} 147 | for i = 1, select("#", ...) do 148 | res[ select(i, ...) ] = true 149 | end 150 | return res 151 | end 152 | 153 | local space_chars = create_set(" ", "\t", "\r", "\n") 154 | local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") 155 | local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") 156 | local literals = create_set("true", "false", "null") 157 | 158 | local literal_map = { 159 | [ "true" ] = true, 160 | [ "false" ] = false, 161 | [ "null" ] = nil, 162 | } 163 | 164 | 165 | local function next_char(str, idx, set, negate) 166 | for i = idx, #str do 167 | if set[str:sub(i, i)] ~= negate then 168 | return i 169 | end 170 | end 171 | return #str + 1 172 | end 173 | 174 | 175 | local function decode_error(str, idx, msg) 176 | local line_count = 1 177 | local col_count = 1 178 | for i = 1, idx - 1 do 179 | col_count = col_count + 1 180 | if str:sub(i, i) == "\n" then 181 | line_count = line_count + 1 182 | col_count = 1 183 | end 184 | end 185 | error( string.format("%s at line %d col %d", msg, line_count, col_count) ) 186 | end 187 | 188 | 189 | local function codepoint_to_utf8(n) 190 | -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa 191 | local f = math.floor 192 | if n <= 0x7f then 193 | return string.char(n) 194 | elseif n <= 0x7ff then 195 | return string.char(f(n / 64) + 192, n % 64 + 128) 196 | elseif n <= 0xffff then 197 | return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) 198 | elseif n <= 0x10ffff then 199 | return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, 200 | f(n % 4096 / 64) + 128, n % 64 + 128) 201 | end 202 | error( string.format("invalid unicode codepoint '%x'", n) ) 203 | end 204 | 205 | 206 | local function parse_unicode_escape(s) 207 | local n1 = tonumber( s:sub(1, 4), 16 ) 208 | local n2 = tonumber( s:sub(7, 10), 16 ) 209 | -- Surrogate pair? 210 | if n2 then 211 | return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) 212 | else 213 | return codepoint_to_utf8(n1) 214 | end 215 | end 216 | 217 | 218 | local function parse_string(str, i) 219 | local res = "" 220 | local j = i + 1 221 | local k = j 222 | 223 | while j <= #str do 224 | local x = str:byte(j) 225 | 226 | if x < 32 then 227 | decode_error(str, j, "control character in string") 228 | 229 | elseif x == 92 then -- `\`: Escape 230 | res = res .. str:sub(k, j - 1) 231 | j = j + 1 232 | local c = str:sub(j, j) 233 | if c == "u" then 234 | local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1) 235 | or str:match("^%x%x%x%x", j + 1) 236 | or decode_error(str, j - 1, "invalid unicode escape in string") 237 | res = res .. parse_unicode_escape(hex) 238 | j = j + #hex 239 | else 240 | if not escape_chars[c] then 241 | decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string") 242 | end 243 | res = res .. escape_char_map_inv[c] 244 | end 245 | k = j + 1 246 | 247 | elseif x == 34 then -- `"`: End of string 248 | res = res .. str:sub(k, j - 1) 249 | return res, j + 1 250 | end 251 | 252 | j = j + 1 253 | end 254 | 255 | decode_error(str, i, "expected closing quote for string") 256 | end 257 | 258 | 259 | local function parse_number(str, i) 260 | local x = next_char(str, i, delim_chars) 261 | local s = str:sub(i, x - 1) 262 | local n = tonumber(s) 263 | if not n then 264 | decode_error(str, i, "invalid number '" .. s .. "'") 265 | end 266 | return n, x 267 | end 268 | 269 | 270 | local function parse_literal(str, i) 271 | local x = next_char(str, i, delim_chars) 272 | local word = str:sub(i, x - 1) 273 | if not literals[word] then 274 | decode_error(str, i, "invalid literal '" .. word .. "'") 275 | end 276 | return literal_map[word], x 277 | end 278 | 279 | 280 | local function parse_array(str, i) 281 | local res = {} 282 | local n = 1 283 | i = i + 1 284 | while 1 do 285 | local x 286 | i = next_char(str, i, space_chars, true) 287 | -- Empty / end of array? 288 | if str:sub(i, i) == "]" then 289 | i = i + 1 290 | break 291 | end 292 | -- Read token 293 | x, i = parse(str, i) 294 | res[n] = x 295 | n = n + 1 296 | -- Next token 297 | i = next_char(str, i, space_chars, true) 298 | local chr = str:sub(i, i) 299 | i = i + 1 300 | if chr == "]" then break end 301 | if chr ~= "," then decode_error(str, i, "expected ']' or ','") end 302 | end 303 | return res, i 304 | end 305 | 306 | 307 | local function parse_object(str, i) 308 | local res = {} 309 | i = i + 1 310 | while 1 do 311 | local key, val 312 | i = next_char(str, i, space_chars, true) 313 | -- Empty / end of object? 314 | if str:sub(i, i) == "}" then 315 | i = i + 1 316 | break 317 | end 318 | -- Read key 319 | if str:sub(i, i) ~= '"' then 320 | decode_error(str, i, "expected string for key") 321 | end 322 | key, i = parse(str, i) 323 | -- Read ':' delimiter 324 | i = next_char(str, i, space_chars, true) 325 | if str:sub(i, i) ~= ":" then 326 | decode_error(str, i, "expected ':' after key") 327 | end 328 | i = next_char(str, i + 1, space_chars, true) 329 | -- Read value 330 | val, i = parse(str, i) 331 | -- Set 332 | res[key] = val 333 | -- Next token 334 | i = next_char(str, i, space_chars, true) 335 | local chr = str:sub(i, i) 336 | i = i + 1 337 | if chr == "}" then break end 338 | if chr ~= "," then decode_error(str, i, "expected '}' or ','") end 339 | end 340 | return res, i 341 | end 342 | 343 | 344 | local char_func_map = { 345 | [ '"' ] = parse_string, 346 | [ "0" ] = parse_number, 347 | [ "1" ] = parse_number, 348 | [ "2" ] = parse_number, 349 | [ "3" ] = parse_number, 350 | [ "4" ] = parse_number, 351 | [ "5" ] = parse_number, 352 | [ "6" ] = parse_number, 353 | [ "7" ] = parse_number, 354 | [ "8" ] = parse_number, 355 | [ "9" ] = parse_number, 356 | [ "-" ] = parse_number, 357 | [ "t" ] = parse_literal, 358 | [ "f" ] = parse_literal, 359 | [ "n" ] = parse_literal, 360 | [ "[" ] = parse_array, 361 | [ "{" ] = parse_object, 362 | } 363 | 364 | 365 | parse = function(str, idx) 366 | local chr = str:sub(idx, idx) 367 | local f = char_func_map[chr] 368 | if f then 369 | return f(str, idx) 370 | end 371 | decode_error(str, idx, "unexpected character '" .. chr .. "'") 372 | end 373 | 374 | 375 | function json.decode(str) 376 | if type(str) ~= "string" then 377 | error("expected argument of type string, got " .. type(str)) 378 | end 379 | local res, idx = parse(str, next_char(str, 1, space_chars, true)) 380 | idx = next_char(str, idx, space_chars, true) 381 | if idx <= #str then 382 | decode_error(str, idx, "trailing garbage") 383 | end 384 | return res 385 | end 386 | 387 | 388 | return json 389 | -------------------------------------------------------------------------------- /server/keymap.lua: -------------------------------------------------------------------------------- 1 | -- vim: set et sw=4: 2 | -- 3 | -- Copyright (c) 2020 Ryan Moeller 4 | -- 5 | -- Permission to use, copy, modify, and distribute this software for any 6 | -- purpose with or without fee is hereby granted, provided that the above 7 | -- copyright notice and this permission notice appear in all copies. 8 | -- 9 | -- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | -- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | -- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | -- ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | -- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | -- ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | -- OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -- 17 | 18 | local misc = require("misc") 19 | 20 | local keymap = keymap or {} 21 | 22 | keymap.VT = "/usr/share/vt/keymaps" 23 | keymap.SYSCONS = "/usr/share/syscons/keymaps" 24 | 25 | function keymap.index(path) 26 | local index = {} 27 | local menu = {} 28 | local font = {} 29 | local f = assert(io.open(path .. "/INDEX.keymaps", "r")) 30 | local text = f:read("*a") 31 | f:close() 32 | for line in text:gmatch("([^\n]+)") do 33 | if line:find("^%s*#") == nil and line:find("^%s*$") == nil then 34 | local layout, lang, desc = line:match("(.*):(.*):(.*)") 35 | if lang == "" then 36 | lang = "en" 37 | end 38 | if layout == "MENU" then 39 | menu[lang] = desc 40 | elseif layout == "FONT" then 41 | font[lang] = desc 42 | else 43 | local list = index[lang] or {} 44 | local file = path .. "/" .. layout 45 | table.insert(list, { file=file, desc=desc }) 46 | index[lang] = list 47 | end 48 | end 49 | end 50 | return index, menu, font 51 | end 52 | 53 | function keymap.setKeymap(layout, variant) 54 | if (variant == "") then 55 | os.execute("XAUTHORITY="..XAUTHORITY.." setxkbmap -display :0 " .. layout) --This is a hack. TODO: figure out how to do this better 56 | else 57 | os.execute("XAUTHORITY="..XAUTHORITY.." setxkbmap -display :0 " .. layout .. " -variant " .. variant) 58 | end 59 | end 60 | 61 | --keymap.XList = {{layout, desc}, ...} 62 | --keymap.XMap = {layout = {{variant, desc}, ...}, ...} 63 | 64 | function getXKeymaps() 65 | local list = {} 66 | local map = {} 67 | local f = assert(io.open("/usr/local/share/X11/xkb/rules/xorg.lst", "r")) 68 | 69 | local state = ""; 70 | for line in f:lines() do 71 | if (line:sub(1, 1) == "!") then 72 | state = line:sub(3); 73 | elseif (state == "layout") then 74 | local layout, desc = line:match("(%a+)%s+(.+)") 75 | if (layout and desc) then 76 | table.insert(list, {layout=layout, desc=desc}) 77 | map[layout] = {{variant="", desc="(none)"}} 78 | end 79 | elseif (state == "variant") then 80 | local variant, layout, desc = line:match("(%g+)%s+(%a+): (.+)") 81 | if (layout and variant and desc) then 82 | table.insert(map[layout], {variant=variant, desc=desc}) 83 | end 84 | end 85 | end 86 | 87 | local function compareLayouts(this, other) 88 | return this.desc < other.desc 89 | end 90 | 91 | table.sort(list, compareLayouts) 92 | keymap.XList = list 93 | keymap.XMap = map 94 | end 95 | 96 | function keymap.getCurrentLayout() 97 | local data_file = io.popen("XAUTHORITY="..XAUTHORITY.." setxkbmap -display :0 -query") 98 | 99 | for line in data_file:lines() do 100 | local layout = line:match("layout:%s+(.*)") 101 | if (layout) then 102 | return layout 103 | end 104 | end 105 | 106 | return "" 107 | end 108 | 109 | function keymap.getCurrentVariant() 110 | local data_file = io.popen("XAUTHORITY="..XAUTHORITY.." setxkbmap -display :0 -query") 111 | 112 | for line in data_file:lines() do 113 | local variant = line:match("variant:%s+(.*)") 114 | if (variant) then 115 | return variant 116 | end 117 | end 118 | 119 | return "" 120 | end 121 | 122 | function keymap.prettyPrint(layout, variant) 123 | local layout_desc, variant_desc 124 | 125 | for _, map in ipairs(keymap.XList) do 126 | if (map.layout == layout) then 127 | layout_desc = map.desc 128 | variant_desc = keymap.XMap[layout].desc 129 | break 130 | end 131 | end 132 | for _, var in ipairs(keymap.XMap[layout]) do 133 | if (var.variant == variant) then 134 | variant_desc = var.desc 135 | break 136 | end 137 | end 138 | 139 | if (variant ~= "") and variant then 140 | return layout_desc .. " - " .. variant_desc 141 | else 142 | return layout_desc 143 | end 144 | end 145 | 146 | getXKeymaps() 147 | 148 | return keymap 149 | -------------------------------------------------------------------------------- /server/lang.lua: -------------------------------------------------------------------------------- 1 | -- vim: set et sw=4: 2 | -- 3 | -- Copyright (c) 2020 Ryan Moeller 4 | -- 5 | -- Permission to use, copy, modify, and distribute this software for any 6 | -- purpose with or without fee is hereby granted, provided that the above 7 | -- copyright notice and this permission notice appear in all copies. 8 | -- 9 | -- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | -- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | -- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | -- ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | -- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | -- ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | -- OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -- 17 | 18 | local lang = lang or {} 19 | 20 | lang.translations = { 21 | ["Add user"] = { 22 | en = "Add user", 23 | fr = "Ajouter un utilisateur", 24 | }, 25 | ["Add users to your system."] = { 26 | en = "Add users to your system.", 27 | }, 28 | ["Change"] = { 29 | en = "Change", 30 | fr = "Changer", 31 | }, 32 | ["Change language"] = { 33 | en = "Change language", 34 | fr = "Changer la langue", 35 | }, 36 | ["Choosing a language also allows the installer to pick reasonable default options."] = { 37 | en = "Choosing a language also allows the installer to pick reasonable default options.", 38 | }, 39 | ["Configure the network"] = { 40 | en = "Configure the network", 41 | }, 42 | ["Configure your keyboard"] = { 43 | en = "Configure your keyboard", 44 | fr = "Configurer le clavier", 45 | }, 46 | ["Configure ZFS"] = { 47 | en = "Configure ZFS", 48 | fr = "Configurer ZFS", 49 | }, 50 | ["Confirm"] = { 51 | en = "Confirm", 52 | fr = "Confirmer", 53 | }, 54 | ["Confirm password"] = { 55 | en = "Confirm password", 56 | }, 57 | ["Confirm root password:"] = { 58 | en = "Confirm root password:", 59 | }, 60 | ["Delete"] = { 61 | en = "Delete", 62 | }, 63 | ["Edit"] = { 64 | en = "Edit", 65 | fr = "Modifier", 66 | }, 67 | ["enter password..."] = { 68 | en = "enter password...", 69 | }, 70 | ["Extra distsets:"] = { 71 | en = "Extra distsets:", 72 | }, 73 | ["Filesystem"] = { 74 | en = "Filesystem", 75 | fr = "Système de fichiers", 76 | }, 77 | ["Full name"] = { 78 | en = "Full name", 79 | fr = "Nom", 80 | }, 81 | ["Groups"] = { 82 | en = "Groups", 83 | fr = "Groupes", 84 | }, 85 | ["Home"] = { 86 | en = "Home", 87 | fr = "Accueil", 88 | }, 89 | ["Hostname"] = { 90 | en = "Hostname", 91 | }, 92 | ["Install"] = { 93 | en = "Install", 94 | fr = "Installer", 95 | }, 96 | ["Install FreeBSD"] = { 97 | en = "Install FreeBSD", 98 | fr = "Installer FreeBSD", 99 | jp = "FreeBSDのインストール" 100 | }, 101 | ["Keyboard"] = { 102 | en = "Keyboard", 103 | fr = "Clavier", 104 | }, 105 | ["Keyboard layout"] = { 106 | en = "Keyboard layout", 107 | }, 108 | ["Keymap"] = { 109 | en = "Keymap", 110 | fr = "Disposition du clavier", 111 | }, 112 | ["Language"] = { 113 | en = "Language", 114 | fr = "Langue", 115 | }, 116 | ["Network"] = { 117 | en = "Network", 118 | }, 119 | ["Packages"] = { 120 | en = "Packages", 121 | fr = "Pacquets", 122 | }, 123 | ["Partition Disks: ZFS"] = { 124 | en = "Partition Disks: ZFS", 125 | }, 126 | ["Password"] = { 127 | en = "Password", 128 | fr = "Mot de passe", 129 | }, 130 | ["Root password"] = { 131 | en = "Root password", 132 | fr = "Mot de passe de utilisateur root", 133 | }, 134 | ["Select..."] = { 135 | en = "Select...", 136 | }, 137 | ["Select installer language"] = { 138 | en = "Select installer language", 139 | }, 140 | ["Select language"] = { 141 | en = "Select language", 142 | fr = "Choisir la langue", 143 | }, 144 | ["Select the packages to be installed on your system."] = { 145 | en = "Select the packages to be installed on your system.", 146 | }, 147 | ["Settings"] = { 148 | en = "Settings", 149 | fr = "Paramètres", 150 | }, 151 | ["Shell"] = { 152 | en = "Shell", 153 | }, 154 | ["System Settings"] = { 155 | en = "System Settings", 156 | fr = "Paramètres système", 157 | }, 158 | ["Timezone"] = { 159 | en = "Timezone", 160 | fr = "Fuseau horaire", 161 | }, 162 | ["Username"] = { 163 | en = "Username", 164 | }, 165 | ["Users"] = { 166 | en = "Users", 167 | fr = "Utilisateurs", 168 | }, 169 | ["Variant"] = { 170 | en = "Variant", 171 | }, 172 | } 173 | 174 | lang.languages = { 175 | en = { 176 | name = "English", 177 | keymap_layout = "us", 178 | keymap_variant = "", 179 | }, 180 | fr = { 181 | name = "Français (INCOMPLETE -- FOR TESTING)", 182 | keymap_layout = "fr", 183 | keymap_variant = "", 184 | }, 185 | jp = { 186 | name = "日本語 (INCOMPLETE -- FOR TESTING)", 187 | keymap_layout = "jp", 188 | keymap_variant = "", 189 | }, 190 | } 191 | 192 | --Add a metatable to all the string translations, so that 193 | --if a translation does not exist it defaults to English. 194 | local metatable = {} 195 | metatable.__index = function(table, key) 196 | return table.en 197 | end 198 | 199 | for _, table in pairs(lang.translations) do 200 | setmetatable(table, metatable) 201 | end 202 | 203 | setmetatable(lang.languages, metatable) 204 | 205 | return lang 206 | -------------------------------------------------------------------------------- /server/misc.lua: -------------------------------------------------------------------------------- 1 | local misc = misc or {} 2 | 3 | function misc.splitString(str, char) 4 | local split_list = {} 5 | 6 | for match in str:gmatch("[^" .. char .. "]+") do 7 | table.insert(split_list, match) 8 | end 9 | return split_list 10 | end 11 | 12 | return misc 13 | -------------------------------------------------------------------------------- /server/network.lua: -------------------------------------------------------------------------------- 1 | -- vim: set et sw=4: 2 | 3 | local misc = require("misc") 4 | local network = network or {} 5 | 6 | local function getInterfaceDesc(interface) 7 | if (interface:match("(%a+)(%d+)")) then 8 | local name, num = interface:match("(%a+)(%d+)") 9 | local desc = io.popen("sysctl -n dev."..name.."."..num..".%desc 2>/dev/null"):read() 10 | return desc 11 | end 12 | return nil 13 | end 14 | 15 | local function checkIPv4() 16 | local output = os.execute("sysctl -N kern.features.inet > /dev/null 2>&1") 17 | network.ipv4 = output 18 | output = os.execute("sysctl -N kern.features.inet6 > /dev/null 2>&1") 19 | network.ipv6 = output 20 | end 21 | 22 | network.temp_file = "/tmp/lua-httpd" 23 | 24 | function network.getInterfaces() 25 | local interfaces = {} 26 | 27 | local ifconfig_output = io.popen("ifconfig -l"):read() 28 | for _, intf in ipairs(misc.splitString(ifconfig_output, " ")) do 29 | if (not (intf == "lo0") and 30 | not (os.execute("ifconfig -g wlan | grep -wq " .. intf))) then 31 | interface = { 32 | name = intf, 33 | desc = getInterfaceDesc(intf), 34 | wireless = false, 35 | } 36 | if (interface.desc) then 37 | table.insert(interfaces, interface) 38 | end 39 | end 40 | end 41 | 42 | local wireless_interfaces = io.popen("sysctl -in net.wlan.devices"):read() 43 | for _, intf in ipairs(misc.splitString(wireless_interfaces, " ")) do 44 | interface = { 45 | name = intf, 46 | desc = getInterfaceDesc(intf), 47 | wireless = true, 48 | } 49 | if (interface.desc) then 50 | table.insert(interfaces, interface) 51 | end 52 | end 53 | 54 | return interfaces 55 | end 56 | 57 | function network.isWireless(interface) 58 | local wireless_interfaces = io.popen("sysctl -in net.wlan.devices"):read() 59 | for _, intf in ipairs(misc.splitString(wireless_interfaces, " ")) do 60 | if (interface == intf) then 61 | return true 62 | end 63 | end 64 | return false 65 | end 66 | 67 | function network.scanWireless() -- (interface) 68 | local networks = {} 69 | local one, two = os.execute("wpa_cli scan >/dev/null 2>&1") 70 | os.execute("sleep 5") 71 | for line in io.popen("wpa_cli scan_result"):lines() do 72 | if line:match("(.*)\t(.*)\t(.*)\t(.*)\t(.*)") then 73 | local bssid, frequency, signal_level, flags, ssid = line:match("(.*)\t(.*)\t(.*)\t(.*)\t(.*)") 74 | local n = { 75 | bssid = bssid, 76 | frequency = frequency, 77 | signal_level = signal_level, 78 | flags = flags, 79 | ssid = ssid, 80 | } 81 | 82 | table.insert(networks, n) 83 | end 84 | end 85 | 86 | return networks 87 | end 88 | 89 | function network.startDaemon() 90 | os.execute("pkill wpa_cli") --TODO is this really okay??? 91 | if (network.action_file) then 92 | network.action_file:close() 93 | end 94 | network.action_file = io.popen("wpa_cli -a "..SRC_DIR.."action-script -B > "..network.temp_file.." 2>&1", "r") 95 | end 96 | 97 | function network.status_from_command() 98 | return io.popen("wpa_cli status"):read("a") --TODO these should probably be closed? 99 | end 100 | 101 | function network.status_from_file() 102 | local the_file = io.open(network.temp_file) 103 | 104 | -- This is either "CONNECTED\n", "DISCONNECTED\n", "TEMP-DISABLED\n", "\n". 105 | local status = the_file:read("*all") 106 | status = status:gsub("%s+", "") 107 | the_file:close() 108 | 109 | -- Clear the status file when we reach a 'final' state, so the next 110 | -- connection attempt doesn't get confused. 111 | if (status == "TEMP-DISABLED" or status == "CONNECTED") then 112 | io.open(network.temp_file, "w"):close() 113 | end 114 | 115 | return status 116 | end 117 | 118 | function network.connectWireless(network, password) 119 | local add_network_output = io.popen("wpa_cli add_network", "r") 120 | local network_id = 0 121 | for line in add_network_output:lines() do 122 | local match = line:match("^%d+$") 123 | if (match) then 124 | network_id = match 125 | end 126 | end 127 | os.execute("echo 'set_network "..network_id.." ssid \""..network.."\"' | wpa_cli") 128 | os.execute("echo 'set_network "..network_id.." psk \""..password.."\"' | wpa_cli") 129 | os.execute("wpa_cli select_network "..network_id) 130 | os.execute("wpa_cli reconnect") 131 | end 132 | 133 | checkIPv4() 134 | 135 | return network 136 | -------------------------------------------------------------------------------- /server/partition.lua: -------------------------------------------------------------------------------- 1 | -- vim: set et sw=4: 2 | -- 3 | -- Copyright (c) 2020 Ryan Moeller 4 | -- 5 | -- Permission to use, copy, modify, and distribute this software for any 6 | -- purpose with or without fee is hereby granted, provided that the above 7 | -- copyright notice and this permission notice appear in all copies. 8 | -- 9 | -- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | -- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | -- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | -- ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | -- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | -- ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | -- OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -- 17 | 18 | local partition = partition or {} 19 | 20 | -- TODO: various workarounds needed for specific hardware 21 | partition.styles = { 22 | { 23 | title = "GPT (BIOS)", 24 | value = "GPT:BIOS", 25 | prefer = function(bootmethod) 26 | return bootmethod == "BIOS" 27 | end, 28 | }, 29 | { 30 | title = "GPT (UEFI)", 31 | value = "GPT:UEFI", 32 | prefer = function(bootmethod) 33 | return false 34 | end, 35 | }, 36 | { 37 | title = "GPT (BIOS+UEFI)", 38 | value = "GPT:BIOS+UEFI", 39 | prefer = function(bootmethod) 40 | return bootmethod == "UEFI" 41 | end, 42 | }, 43 | { 44 | title = "GPT + Active (BIOS)", 45 | value = "GPT+ACTIVE:BIOS", 46 | prefer = function(bootmethod) 47 | return false 48 | end, 49 | }, 50 | { 51 | title = "GPT + Lenovo Fix (BIOS)", 52 | value = "GPT+LENOVOFIX:BIOS", 53 | prefer = function(bootmethod) 54 | return false 55 | end, 56 | }, 57 | } 58 | 59 | return partition 60 | -------------------------------------------------------------------------------- /server/pkgsets.lua: -------------------------------------------------------------------------------- 1 | local pkgsets = pkgsets or {} 2 | 3 | pkgsets.pkgsets = { 4 | {name = "base", name_human = "Base", vital = true}, 5 | {name = "kernel", name_human = "Kernel", vital = true}, 6 | {name = "debug", name_human = "Debug"}, 7 | {name = "lib32", name_human = "Lib32"}, 8 | {name = "development", name_human = "Development"}, 9 | } 10 | 11 | return pkgsets 12 | -------------------------------------------------------------------------------- /server/service.lua: -------------------------------------------------------------------------------- 1 | -- vim: set et sw=4: 2 | -- 3 | -- Copyright (c) 2020 Ryan Moeller 4 | -- 5 | -- Permission to use, copy, modify, and distribute this software for any 6 | -- purpose with or without fee is hereby granted, provided that the above 7 | -- copyright notice and this permission notice appear in all copies. 8 | -- 9 | -- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | -- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | -- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | -- ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | -- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | -- ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | -- OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -- 17 | 18 | local service = service or {} 19 | 20 | service.menu = { 21 | { 22 | name = "local_unbound", 23 | description = "Local caching validating resolver", 24 | default = false, 25 | }, 26 | { 27 | name = "sshd", 28 | description = "Secure shell daemon", 29 | default = true, 30 | }, 31 | { 32 | name = "moused", 33 | description = "PS/2 mouse pointer on console", 34 | default = false, 35 | }, 36 | { 37 | name = "ntpdate", 38 | description = "Synchronize system and network time at bootime", 39 | default = false, 40 | }, 41 | { 42 | name = "ntpd", 43 | description = "Synchronize system and network time", 44 | default = false, 45 | }, 46 | { 47 | name = "powerd", 48 | description = "Adjust CPU frequency dynamically if supported", 49 | default = false, 50 | }, 51 | { 52 | name = "dumpdev", 53 | description = "Enable kernel crash dumps to /var/crash", 54 | default = true, 55 | }, 56 | } 57 | 58 | return service 59 | -------------------------------------------------------------------------------- /server/shell.lua: -------------------------------------------------------------------------------- 1 | -- vim: set et sw=4: 2 | -- 3 | -- Copyright (c) 2020 Ryan Moeller 4 | -- 5 | -- Permission to use, copy, modify, and distribute this software for any 6 | -- purpose with or without fee is hereby granted, provided that the above 7 | -- copyright notice and this permission notice appear in all copies. 8 | -- 9 | -- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | -- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | -- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | -- ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | -- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | -- ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | -- OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -- 17 | 18 | local shell = shell or {} 19 | 20 | shell.list = { 21 | "/bin/sh", 22 | "/bin/tcsh", 23 | "/bin/csh", 24 | } 25 | 26 | return shell 27 | -------------------------------------------------------------------------------- /server/template.lua: -------------------------------------------------------------------------------- 1 | -- vim: set et sw=4: 2 | -- lua-resty-template (modified to remove external dependencies) 3 | --[[ 4 | Copyright (c) 2014 - 2020 Aapo Talvensaari 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without modification, 8 | are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above copyright notice, this 14 | list of conditions and the following disclaimer in the documentation and/or 15 | other materials provided with the distribution. 16 | 17 | * Neither the name of the {organization} nor the names of its 18 | contributors may be used to endorse or promote products derived from 19 | this software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 22 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 23 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 25 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 26 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 27 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 28 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 30 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | ]]-- 32 | 33 | local setmetatable = setmetatable 34 | local loadstring = loadstring 35 | local tostring = tostring 36 | local setfenv = setfenv 37 | local require = require 38 | local concat = table.concat 39 | local assert = assert 40 | local write = io.write 41 | local pcall = pcall 42 | local phase 43 | local open = io.open 44 | local load = load 45 | local type = type 46 | local dump = string.dump 47 | local find = string.find 48 | local gsub = string.gsub 49 | local byte = string.byte 50 | local null 51 | local sub = string.sub 52 | local var 53 | 54 | local _VERSION = _VERSION 55 | local _ENV = _ENV -- luacheck: globals _ENV 56 | local _G = _G 57 | 58 | local HTML_ENTITIES = { 59 | ["&"] = "&", 60 | ["<"] = "<", 61 | [">"] = ">", 62 | ['"'] = """, 63 | ["'"] = "'", 64 | ["/"] = "/" 65 | } 66 | 67 | local CODE_ENTITIES = { 68 | ["{"] = "{", 69 | ["}"] = "}", 70 | ["&"] = "&", 71 | ["<"] = "<", 72 | [">"] = ">", 73 | ['"'] = """, 74 | ["'"] = "'", 75 | ["/"] = "/" 76 | } 77 | 78 | local VAR_PHASES 79 | 80 | local ESC = byte("\27") 81 | local NUL = byte("\0") 82 | local HT = byte("\t") 83 | local VT = byte("\v") 84 | local LF = byte("\n") 85 | local SOL = byte("/") 86 | local BSOL = byte("\\") 87 | local SP = byte(" ") 88 | local AST = byte("*") 89 | local NUM = byte("#") 90 | local LPAR = byte("(") 91 | local LSQB = byte("[") 92 | local LCUB = byte("{") 93 | local MINUS = byte("-") 94 | local PERCNT = byte("%") 95 | 96 | local EMPTY = "" 97 | 98 | local VIEW_ENV 99 | if _VERSION == "Lua 5.1" then 100 | VIEW_ENV = { __index = function(t, k) 101 | return t.context[k] or t.template[k] or _G[k] 102 | end } 103 | else 104 | VIEW_ENV = { __index = function(t, k) 105 | return t.context[k] or t.template[k] or _ENV[k] 106 | end } 107 | end 108 | 109 | local newtab 110 | do 111 | local ok 112 | ok, newtab = pcall(require, "table.new") 113 | if not ok then newtab = function() return {} end end 114 | end 115 | 116 | local function enabled(val) 117 | if val == nil then return true end 118 | return val == true or (val == "1" or val == "true" or val == "on") 119 | end 120 | 121 | local function trim(s) 122 | return gsub(gsub(s, "^%s+", EMPTY), "%s+$", EMPTY) 123 | end 124 | 125 | local function rpos(view, s) 126 | while s > 0 do 127 | local c = byte(view, s, s) 128 | if c == SP or c == HT or c == VT or c == NUL then 129 | s = s - 1 130 | else 131 | break 132 | end 133 | end 134 | return s 135 | end 136 | 137 | local function escaped(view, s) 138 | if s > 1 and byte(view, s - 1, s - 1) == BSOL then 139 | if s > 2 and byte(view, s - 2, s - 2) == BSOL then 140 | return false, 1 141 | else 142 | return true, 1 143 | end 144 | end 145 | return false, 0 146 | end 147 | 148 | local function read_file(path) 149 | local file, err = open(path, "rb") 150 | if not file then return nil, err end 151 | local content 152 | content, err = file:read "*a" 153 | file:close() 154 | return content, err 155 | end 156 | 157 | local function load_view(template) 158 | return function(view, plain) 159 | if plain == true then return view end 160 | local path, root = view, template.root 161 | if root and root ~= EMPTY then 162 | if byte(root, -1) == SOL then root = sub(root, 1, -2) end 163 | if byte(view, 1) == SOL then path = sub(view, 2) end 164 | path = root .. "/" .. path 165 | end 166 | return plain == false and assert(read_file(path)) or read_file(path) or view 167 | end 168 | end 169 | 170 | local function load_file(func) 171 | return function(view) return func(view, false) end 172 | end 173 | 174 | local function load_string(func) 175 | return function(view) return func(view, true) end 176 | end 177 | 178 | local function loader(template) 179 | return function(view) 180 | return assert(load(view, nil, nil, setmetatable({ template = template }, VIEW_ENV))) 181 | end 182 | end 183 | 184 | local function visit(visitors, content, tag, name) 185 | if not visitors then 186 | return content 187 | end 188 | 189 | for i = 1, visitors.n do 190 | content = visitors[i](content, tag, name) 191 | end 192 | 193 | return content 194 | end 195 | 196 | local function new(template, safe) 197 | template = template or newtab(0, 26) 198 | 199 | template._VERSION = "2.0" 200 | template.cache = {} 201 | template.load = load_view(template) 202 | template.load_file = load_file(template.load) 203 | template.load_string = load_string(template.load) 204 | template.print = write 205 | 206 | local load_chunk = loader(template) 207 | 208 | local caching 209 | if VAR_PHASES and VAR_PHASES[phase()] then 210 | caching = enabled(var.template_cache) 211 | else 212 | caching = true 213 | end 214 | 215 | local visitors 216 | function template.visit(func) 217 | if not visitors then 218 | visitors = { func, n = 1 } 219 | return 220 | end 221 | visitors.n = visitors.n + 1 222 | visitors[visitors.n] = func 223 | end 224 | 225 | function template.caching(enable) 226 | if enable ~= nil then caching = enable == true end 227 | return caching 228 | end 229 | 230 | function template.output(s) 231 | if s == nil or s == null then return EMPTY end 232 | if type(s) == "function" then return template.output(s()) end 233 | return tostring(s) 234 | end 235 | 236 | function template.escape(s, c) 237 | if type(s) == "string" then 238 | if c then return gsub(s, "[}{\">/<'&]", CODE_ENTITIES) end 239 | return gsub(s, "[\">/<'&]", HTML_ENTITIES) 240 | end 241 | return template.output(s) 242 | end 243 | 244 | function template.new(view, layout) 245 | local vt = type(view) 246 | 247 | if vt == "boolean" then return new(nil, view) end 248 | if vt == "table" then return new(view, safe) end 249 | if vt == "nil" then return new(nil, safe) end 250 | 251 | local render 252 | local process 253 | if layout then 254 | if type(layout) == "table" then 255 | render = function(self, context) 256 | context = context or self 257 | context.blocks = context.blocks or {} 258 | context.view = template.process(view, context) 259 | layout.blocks = context.blocks or {} 260 | layout.view = context.view or EMPTY 261 | layout:render() 262 | end 263 | process = function(self, context) 264 | context = context or self 265 | context.blocks = context.blocks or {} 266 | context.view = template.process(view, context) 267 | layout.blocks = context.blocks or {} 268 | layout.view = context.view 269 | return tostring(layout) 270 | end 271 | else 272 | render = function(self, context) 273 | context = context or self 274 | context.blocks = context.blocks or {} 275 | context.view = template.process(view, context) 276 | template.render(layout, context) 277 | end 278 | process = function(self, context) 279 | context = context or self 280 | context.blocks = context.blocks or {} 281 | context.view = template.process(view, context) 282 | return template.process(layout, context) 283 | end 284 | end 285 | else 286 | render = function(self, context) 287 | return template.render(view, context or self) 288 | end 289 | process = function(self, context) 290 | return template.process(view, context or self) 291 | end 292 | end 293 | 294 | if safe then 295 | return setmetatable({ 296 | render = function(...) 297 | local ok, err = pcall(render, ...) 298 | if not ok then 299 | return nil, err 300 | end 301 | end, 302 | process = function(...) 303 | local ok, output = pcall(process, ...) 304 | if not ok then 305 | return nil, output 306 | end 307 | return output 308 | end, 309 | }, { 310 | __tostring = function(...) 311 | local ok, output = pcall(process, ...) 312 | if not ok then 313 | return "" 314 | end 315 | return output 316 | end }) 317 | end 318 | 319 | return setmetatable({ 320 | render = render, 321 | process = process 322 | }, { 323 | __tostring = process 324 | }) 325 | end 326 | 327 | function template.precompile(view, path, strip, plain) 328 | local chunk = dump(template.compile(view, nil, plain), strip ~= false) 329 | if path then 330 | local file = open(path, "wb") 331 | file:write(chunk) 332 | file:close() 333 | end 334 | return chunk 335 | end 336 | 337 | function template.precompile_string(view, path, strip) 338 | return template.precompile(view, path, strip, true) 339 | end 340 | 341 | function template.precompile_file(view, path, strip) 342 | return template.precompile(view, path, strip, false) 343 | end 344 | 345 | function template.compile(view, cache_key, plain) 346 | assert(view, "view was not provided for template.compile(view, cache_key, plain)") 347 | if cache_key == "no-cache" then 348 | return load_chunk(template.parse(view, plain)), false 349 | end 350 | cache_key = cache_key or view 351 | local cache = template.cache 352 | if cache[cache_key] then return cache[cache_key], true end 353 | local func = load_chunk(template.parse(view, plain)) 354 | if caching then cache[cache_key] = func end 355 | return func, false 356 | end 357 | 358 | function template.compile_file(view, cache_key) 359 | return template.compile(view, cache_key, false) 360 | end 361 | 362 | function template.compile_string(view, cache_key) 363 | return template.compile(view, cache_key, true) 364 | end 365 | 366 | function template.parse(view, plain) 367 | assert(view, "view was not provided for template.parse(view, plain)") 368 | if plain ~= true then 369 | view = template.load(view, plain) 370 | if byte(view, 1, 1) == ESC then return view end 371 | end 372 | local j = 2 373 | local c = {[[ 374 | context=... or {} 375 | local ___,blocks,layout={},blocks or {} 376 | local function include(v, c) return template.process(v, c or context) end 377 | local function echo(...) for i=1,select("#", ...) do ___[#___+1] = tostring(select(i, ...)) end end 378 | ]] } 379 | local i, s = 1, find(view, "{", 1, true) 380 | while s do 381 | local t, p = byte(view, s + 1, s + 1), s + 2 382 | if t == LCUB then 383 | local e = find(view, "}}", p, true) 384 | if e then 385 | local z, w = escaped(view, s) 386 | if i < s - w then 387 | c[j] = "___[#___+1]=[=[\n" 388 | c[j+1] = visit(visitors, sub(view, i, s - 1 - w)) 389 | c[j+2] = "]=]\n" 390 | j=j+3 391 | end 392 | if z then 393 | i = s 394 | else 395 | c[j] = "___[#___+1]=template.escape(" 396 | c[j+1] = visit(visitors, trim(sub(view, p, e - 1)), "{") 397 | c[j+2] = ")\n" 398 | j=j+3 399 | s, i = e + 1, e + 2 400 | end 401 | end 402 | elseif t == AST then 403 | local e = find(view, "*}", p, true) 404 | if e then 405 | local z, w = escaped(view, s) 406 | if i < s - w then 407 | c[j] = "___[#___+1]=[=[\n" 408 | c[j+1] = visit(visitors, sub(view, i, s - 1 - w)) 409 | c[j+2] = "]=]\n" 410 | j=j+3 411 | end 412 | if z then 413 | i = s 414 | else 415 | c[j] = "___[#___+1]=template.output(" 416 | c[j+1] = visit(visitors, trim(sub(view, p, e - 1)), "*") 417 | c[j+2] = ")\n" 418 | j=j+3 419 | s, i = e + 1, e + 2 420 | end 421 | end 422 | elseif t == PERCNT then 423 | local e = find(view, "%}", p, true) 424 | if e then 425 | local z, w = escaped(view, s) 426 | if z then 427 | if i < s - w then 428 | c[j] = "___[#___+1]=[=[\n" 429 | c[j+1] = visit(visitors, sub(view, i, s - 1 - w)) 430 | c[j+2] = "]=]\n" 431 | j=j+3 432 | end 433 | i = s 434 | else 435 | local n = e + 2 436 | if byte(view, n, n) == LF then 437 | n = n + 1 438 | end 439 | local r = rpos(view, s - 1) 440 | if i <= r then 441 | c[j] = "___[#___+1]=[=[\n" 442 | c[j+1] = visit(visitors, sub(view, i, r)) 443 | c[j+2] = "]=]\n" 444 | j=j+3 445 | end 446 | c[j] = visit(visitors, trim(sub(view, p, e - 1)), "%") 447 | c[j+1] = "\n" 448 | j=j+2 449 | s, i = n - 1, n 450 | end 451 | end 452 | elseif t == LPAR then 453 | local e = find(view, ")}", p, true) 454 | if e then 455 | local z, w = escaped(view, s) 456 | if i < s - w then 457 | c[j] = "___[#___+1]=[=[\n" 458 | c[j+1] = visit(visitors, sub(view, i, s - 1 - w)) 459 | c[j+2] = "]=]\n" 460 | j=j+3 461 | end 462 | if z then 463 | i = s 464 | else 465 | local f = visit(visitors, sub(view, p, e - 1), "(") 466 | local x = find(f, ",", 2, true) 467 | if x then 468 | c[j] = "___[#___+1]=include([=[" 469 | c[j+1] = trim(sub(f, 1, x - 1)) 470 | c[j+2] = "]=]," 471 | c[j+3] = trim(sub(f, x + 1)) 472 | c[j+4] = ")\n" 473 | j=j+5 474 | else 475 | c[j] = "___[#___+1]=include([=[" 476 | c[j+1] = trim(f) 477 | c[j+2] = "]=])\n" 478 | j=j+3 479 | end 480 | s, i = e + 1, e + 2 481 | end 482 | end 483 | elseif t == LSQB then 484 | local e = find(view, "]}", p, true) 485 | if e then 486 | local z, w = escaped(view, s) 487 | if i < s - w then 488 | c[j] = "___[#___+1]=[=[\n" 489 | c[j+1] = visit(visitors, sub(view, i, s - 1 - w)) 490 | c[j+2] = "]=]\n" 491 | j=j+3 492 | end 493 | if z then 494 | i = s 495 | else 496 | c[j] = "___[#___+1]=include(" 497 | c[j+1] = visit(visitors, trim(sub(view, p, e - 1)), "[") 498 | c[j+2] = ")\n" 499 | j=j+3 500 | s, i = e + 1, e + 2 501 | end 502 | end 503 | elseif t == MINUS then 504 | local e = find(view, "-}", p, true) 505 | if e then 506 | local x, y = find(view, sub(view, s, e + 1), e + 2, true) 507 | if x then 508 | local z, w = escaped(view, s) 509 | if z then 510 | if i < s - w then 511 | c[j] = "___[#___+1]=[=[\n" 512 | c[j+1] = visit(visitors, sub(view, i, s - 1 - w)) 513 | c[j+2] = "]=]\n" 514 | j=j+3 515 | end 516 | i = s 517 | else 518 | y = y + 1 519 | x = x - 1 520 | if byte(view, y, y) == LF then 521 | y = y + 1 522 | end 523 | local b = trim(sub(view, p, e - 1)) 524 | if b == "verbatim" or b == "raw" then 525 | if i < s - w then 526 | c[j] = "___[#___+1]=[=[\n" 527 | c[j+1] = visit(visitors, sub(view, i, s - 1 - w)) 528 | c[j+2] = "]=]\n" 529 | j=j+3 530 | end 531 | c[j] = "___[#___+1]=[=[" 532 | c[j+1] = visit(visitors, sub(view, e + 2, x)) 533 | c[j+2] = "]=]\n" 534 | j=j+3 535 | else 536 | if byte(view, x, x) == LF then 537 | x = x - 1 538 | end 539 | local r = rpos(view, s - 1) 540 | if i <= r then 541 | c[j] = "___[#___+1]=[=[\n" 542 | c[j+1] = visit(visitors, sub(view, i, r)) 543 | c[j+2] = "]=]\n" 544 | j=j+3 545 | end 546 | c[j] = 'blocks["' 547 | c[j+1] = b 548 | c[j+2] = '"]=include[=[' 549 | c[j+3] = visit(visitors, sub(view, e + 2, x), "-", b) 550 | c[j+4] = "]=]\n" 551 | j=j+5 552 | end 553 | s, i = y - 1, y 554 | end 555 | end 556 | end 557 | elseif t == NUM then 558 | local e = find(view, "#}", p, true) 559 | if e then 560 | local z, w = escaped(view, s) 561 | if i < s - w then 562 | c[j] = "___[#___+1]=[=[\n" 563 | c[j+1] = visit(visitors, sub(view, i, s - 1 - w)) 564 | c[j+2] = "]=]\n" 565 | j=j+3 566 | end 567 | if z then 568 | i = s 569 | else 570 | e = e + 2 571 | if byte(view, e, e) == LF then 572 | e = e + 1 573 | end 574 | s, i = e - 1, e 575 | end 576 | end 577 | end 578 | s = find(view, "{", s + 1, true) 579 | end 580 | s = sub(view, i) 581 | if s and s ~= EMPTY then 582 | c[j] = "___[#___+1]=[=[\n" 583 | c[j+1] = visit(visitors, s) 584 | c[j+2] = "]=]\n" 585 | j=j+3 586 | end 587 | c[j] = "return layout and include(layout,setmetatable({view=table.concat(___),blocks=blocks},{__index=context})) or table.concat(___)" -- luacheck: ignore 588 | return concat(c) 589 | end 590 | 591 | function template.parse_file(view) 592 | return template.parse(view, false) 593 | end 594 | 595 | function template.parse_string(view) 596 | return template.parse(view, true) 597 | end 598 | 599 | function template.process(view, context, cache_key, plain) 600 | assert(view, "view was not provided for template.process(view, context, cache_key, plain)") 601 | return template.compile(view, cache_key, plain)(context) 602 | end 603 | 604 | function template.process_file(view, context, cache_key) 605 | assert(view, "view was not provided for template.process_file(view, context, cache_key)") 606 | return template.compile(view, cache_key, false)(context) 607 | end 608 | 609 | function template.process_string(view, context, cache_key) 610 | assert(view, "view was not provided for template.process_string(view, context, cache_key)") 611 | return template.compile(view, cache_key, true)(context) 612 | end 613 | 614 | function template.render(view, context, cache_key, plain) 615 | assert(view, "view was not provided for template.render(view, context, cache_key, plain)") 616 | template.print(template.process(view, context, cache_key, plain)) 617 | end 618 | 619 | function template.render_file(view, context, cache_key) 620 | assert(view, "view was not provided for template.render_file(view, context, cache_key)") 621 | template.render(view, context, cache_key, false) 622 | end 623 | 624 | function template.render_string(view, context, cache_key) 625 | assert(view, "view was not provided for template.render_string(view, context, cache_key)") 626 | template.render(view, context, cache_key, true) 627 | end 628 | 629 | if safe then 630 | return setmetatable({}, { 631 | __index = function(_, k) 632 | if type(template[k]) == "function" then 633 | return function(...) 634 | local ok, a, b = pcall(template[k], ...) 635 | if not ok then 636 | return nil, a 637 | end 638 | return a, b 639 | end 640 | end 641 | return template[k] 642 | end, 643 | __new_index = function(_, k, v) 644 | template[k] = v 645 | end, 646 | }) 647 | end 648 | 649 | return template 650 | end 651 | 652 | return new() 653 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | :first-child { 6 | margin-top: 0; 7 | } 8 | 9 | :last-child { 10 | margin-bottom: 0; 11 | } 12 | 13 | body { 14 | background-color: #eee; 15 | font-family: Helvetica, sans-serif; 16 | line-height:1.4; 17 | padding: 1rem; 18 | word-wrap:break-word; 19 | } 20 | 21 | .header { 22 | display: flex; 23 | justify-content: space-between; 24 | align-items: baseline; 25 | } 26 | 27 | .title { 28 | margin: 0; 29 | font-weight: normal; 30 | } 31 | 32 | h2 { 33 | font-size: 1.1rem; 34 | margin: 1rem 0; 35 | } 36 | 37 | 38 | /* Styling tables is a pain. Beware! */ 39 | table { 40 | border-collapse:collapse; 41 | width:100%; 42 | margin: 1rem 0; 43 | } 44 | th { 45 | text-align: left; 46 | text-transform: uppercase; 47 | font-size: 0.8rem; 48 | } 49 | th, 50 | td { 51 | padding: 0.5rem 1rem; 52 | } 53 | tr:first-child td, 54 | tr:first-child th { 55 | padding-top: 1rem; 56 | } 57 | tr:last-child td, 58 | tr:last-child th { 59 | padding-bottom: 1rem; 60 | } 61 | thead tr:last-child td, 62 | thead tr:last-child th { 63 | padding-bottom: 0.5rem; 64 | } 65 | thead + tbody tr:first-child td, 66 | thead + tbody tr:first-child th{ 67 | padding-top: 0.5rem; 68 | } 69 | /* Resume sanity. */ 70 | 71 | hr { 72 | border: none; 73 | border-bottom: 1px solid #bbb; 74 | margin: 1.5rem 0; 75 | } 76 | 77 | label { 78 | display: table; 79 | margin: 1.5em 0; 80 | } 81 | 82 | select { 83 | display: inline; 84 | } 85 | 86 | textarea { 87 | margin: 1rem 0; 88 | } 89 | 90 | .button { 91 | padding: 0.5em 1em; 92 | border-radius:5px; 93 | color: #1e3799; 94 | background: white; 95 | border: 1px solid currentColor; 96 | cursor: pointer; 97 | display: inline-block; 98 | text-decoration: none; 99 | font-size: 0.9rem; 100 | } 101 | 102 | .button:disabled { 103 | opacity: 0.4; 104 | cursor: not-allowed; 105 | } 106 | 107 | .button.right-side { 108 | margin-left: 1rem; 109 | } 110 | 111 | .button.primary { 112 | background: #1e3799; 113 | color: white; 114 | } 115 | 116 | .button.tertiary { 117 | border-color: rgba(0, 0, 0, 0); 118 | } 119 | 120 | a { 121 | color: #1e3799; 122 | text-decoration: none; 123 | } 124 | 125 | .link { 126 | text-transform: uppercase; 127 | text-decoration: none; 128 | color: #1e3799; 129 | padding: 0; 130 | font-size: 1rem; 131 | display: inline; 132 | background: none; 133 | border: none; 134 | cursor: pointer; 135 | font-weight: bold; 136 | } 137 | 138 | .wrapper { 139 | width: 80rem; 140 | max-width: 100%; 141 | margin: 0 auto; 142 | 143 | } 144 | 145 | .breadcrumbs { 146 | font-size: 0.8rem; 147 | padding-left: 0; 148 | padding-bottom: 1rem; 149 | border-bottom: 1px solid #bbb; 150 | } 151 | 152 | .section, 153 | .section-box { 154 | margin: 1rem 0; 155 | } 156 | 157 | .section-box, 158 | .section-box-nested { 159 | padding: 1rem; 160 | border: 1px solid #bbb; 161 | background: white; 162 | } 163 | 164 | .section-box-nested { 165 | padding: 0; 166 | border-bottom: none; 167 | } 168 | 169 | .sub-box { 170 | padding: 1rem; 171 | } 172 | 173 | .advanced-opts-box { 174 | background: #eeeeee; 175 | padding: 1rem; 176 | } 177 | 178 | .sub-box, 179 | .sub-box-table { 180 | border-bottom: 1px solid #bbb; 181 | } 182 | 183 | .section-box-grid { 184 | display: flex; 185 | padding: 0; 186 | } 187 | 188 | .grid-left { 189 | width: 30%; 190 | padding: 1rem; 191 | } 192 | 193 | .grid-right { 194 | width: 70%; 195 | padding: 1rem; 196 | } 197 | 198 | .change-link { 199 | margin-left: 1rem; 200 | font-size: 0.8rem; 201 | font-weight: bold; 202 | } 203 | 204 | .link.delete { 205 | color: #888; 206 | } 207 | 208 | .description { 209 | font-style: italic; 210 | font-size: 0.8rem; 211 | } 212 | 213 | /* 214 | .users thead { 215 | border-bottom: 1px solid #bbb; 216 | } 217 | 218 | .users tbody tr + tr { 219 | border-top: 1px solid #bbb; 220 | } 221 | */ 222 | 223 | .input-message { 224 | font-style: italic; 225 | padding-left: 1rem; 226 | } 227 | 228 | .grouped { 229 | margin: 0; 230 | } 231 | 232 | .label { 233 | font-size: 0.8rem; 234 | text-transform: uppercase; 235 | display: block; 236 | } 237 | 238 | .inline { 239 | display: inline-block; 240 | margin: 0; 241 | } 242 | 243 | -------------------------------------------------------------------------------- /views/add_user.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Install FreeBSD 7 | 8 | 10 | 11 | 12 | 46 |
47 | 48 | 49 | {{lang["Home"][cur_lang]}} > {{lang["Add user"][cur_lang]}} 50 | 51 | 52 |
53 |

{{lang["Add user"][cur_lang]}}

54 |
55 | 56 |
57 |
58 |
59 | 65 | 66 | 71 |
72 | 73 |
74 | 75 | 81 | 87 | 88 |
89 | Other Groups 90 | 92 |

Join any other groups you would like. Separate groups with a space.

93 | 94 | 102 |
103 |
104 | 105 |
106 | 111 | 112 | 118 |
119 |
120 |
121 |
122 |
123 | 124 |
125 |
126 |
127 |
128 |
129 | 130 | 131 | -------------------------------------------------------------------------------- /views/keymap.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{lang["Install FreeBSD"][cur_lang]}} 7 | 8 | 9 | 10 | 59 |
60 | 61 | {{lang["Home"][cur_lang]}} > {{lang["Keyboard"][cur_lang]}} 62 | 63 | 64 |
65 |

{{lang["Configure your keyboard"][cur_lang]}}

66 |
67 | 68 |
69 | 79 | 80 | 88 | 89 | 90 |
91 |
92 | Return 93 |
94 |
95 | 96 | 97 | -------------------------------------------------------------------------------- /views/lang.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{lang["Install FreeBSD"][cur_lang]}} 7 | 8 | 9 | 10 |
11 | 12 | {{lang["Home"][cur_lang]}} > {{lang["Select language"][cur_lang]}} 13 | 14 | 15 |
16 |

{{lang["Select installer language"][cur_lang]}}

17 |
18 | 19 |
20 |
21 |
22 |
23 | 31 |
32 |
33 |

{{lang["Choosing a language also allows the installer to pick reasonable default options."][cur_lang]}}

34 | 35 |
36 |
37 |
38 |
39 | 40 |
41 |
42 |
43 |
44 | 45 |
46 | 47 | 48 | -------------------------------------------------------------------------------- /views/main_page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{lang["Install FreeBSD"][cur_lang]}} 7 | 8 | 9 | 10 | 53 |
54 |
55 | 56 |
57 |
58 |

{{lang["Install FreeBSD"][cur_lang]}}

59 | 🌍 {{lang["Change language"][cur_lang]}} 60 |
61 |
62 | 63 |
64 |
65 |

1. {{lang["Settings"][cur_lang]}}

66 |
67 | 68 |
69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 |
{{lang["Keymap"][cur_lang]}}{{keymap_string}}{{lang["Change"][cur_lang]}}
{{lang["Timezone"][cur_lang]}}Setting goes here
79 |
80 |
81 |
82 |
83 |

2. {{lang["Network"][cur_lang]}}

84 |
85 |
86 | {% if not network_string then %} 87 |

Set up a connection to the internet.

88 | {% end %} 89 |
{{network_string}}
90 | {% if network_string then %} 91 | Reconfigure the network 92 | {% else %} 93 | {{lang["Configure the network"][cur_lang]}} 94 | {% end %} 95 |
96 |
97 |
98 |
99 |

3. {{lang["Filesystem"][cur_lang]}}

100 |
101 | 102 | {% if next(selected_disks) then %} 103 |
104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | {% for dev, info in pairsByKeys(disks) do %} 113 | {% if selected_disks[dev] then %} 114 | 115 | 116 | 117 | 118 | {% end %} 119 | {% end %} 120 | 121 |
NameSize
{{info["Disk descr."]}}{{info["mediasize in bytes human"]}}
122 |
123 | {% end %} 124 | 125 |
126 | {% if next(selected_disks) then %} 127 | Reconfigure ZFS 128 | {% else %} 129 |

Choose the disks that FreeBSD should use.

130 | {{lang["Configure ZFS"][cur_lang]}} 131 | {% end %} 132 |
133 |
134 |
135 |
136 |

4. {{lang["Users"][cur_lang]}}

137 |
138 | {% if #users ~= 0 then %} 139 |
140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | {% for _, usr in ipairs(users) do %} 153 | 154 | 155 | 156 | 157 | 158 | 159 | 165 | 166 | {% end %} 167 | 168 |
{{lang["Username"][cur_lang]}}{{lang["Full name"][cur_lang]}}{{lang["Groups"][cur_lang]}}{{lang["Shell"][cur_lang]}}
{{usr.username}}{{usr.full_name}}{{usr.groups}}{{usr.shell}}{{lang["Edit"][cur_lang]}} 160 |
161 | 162 | 163 |
164 |
169 |
170 | {% end %} 171 |
172 | {% if #users == 0 then %} 173 |

You should add at least one user for everyday use.

174 | + {{lang["Add user"][cur_lang]}} 175 | {% else %} 176 | + Add another user 177 | {% end %} 178 |
179 |
180 | 181 |
182 |
183 |

5. {{lang["Packages"][cur_lang]}}

184 |
185 | 186 |
187 |

{{lang["Select the packages to be installed on your system."][cur_lang]}}

188 | {% for _, pkgset in ipairs(packages) do %} 189 | 195 | {% end %} 196 |
197 |
198 | 199 |
200 | 201 | 202 | 203 |
204 |
205 |

6. {{lang["System Settings"][cur_lang]}}

206 |
207 | 208 |
209 | 215 |
216 |
217 | 223 |
224 |
225 | 226 |
227 |
228 |
229 | 230 |
231 |
232 |
233 | 234 |
235 |
236 |
237 | 238 | 239 | -------------------------------------------------------------------------------- /views/network.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Install FreeBSD 8 | 9 | 10 | 148 |
149 | 150 | {{lang["Home"][cur_lang]}} > {{lang["Network"][cur_lang]}} 151 | 152 | 153 |
154 |

Configure the Network

155 | 156 |

NOTE: This network selector currently only supports wireless interfaces, and only with WPA2, as that is all I have access to right now.

157 |
158 | 159 |
160 |
161 | 172 |
173 | 174 | 191 |
192 |
193 | 194 |
195 |
196 | 197 | 198 | -------------------------------------------------------------------------------- /views/zfs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Install FreeBSD 8 | 10 | 11 | 12 | 49 |
50 | 51 | {{lang["Home"][cur_lang]}} > {{lang["Partition Disks: ZFS"][cur_lang]}} 52 | 53 | 54 |
55 |

{{lang["Partition Disks: ZFS"][cur_lang]}}

56 |
57 | 58 |
59 |
60 |
61 |

62 | Select the disks to use for FreeBSD 63 |

64 | {% for dev, info in pairsByKeys(disks) do %} 65 | 72 | {% end %} 73 |
74 | 75 |
76 | 89 |
90 |
91 | 92 |
93 |

94 | 95 |
96 |
97 | 98 |
99 |
100 |
101 |
102 |
103 | 104 | 105 | --------------------------------------------------------------------------------