Project homepage Mailing List  Warmcat.com  API Docs  Github Mirror 
    npro  
 Modern all-safe Rust Network Protocol library supporting h1, h2, h3, ws, wt sans-IO and with socket IO + tls
git clone https://npro.rs/repo/npro
 
root / src / builder / b-suspender.c
Author[]Andy Green <andy@warmcat.com> 2020-07-09 14:05 UTC
Committer[]Andy Green <andy@warmcat.com> 2020-07-09 14:05 UTC
Treedc651ecb194140764fe5969db47c6013ef9a64bd   Raw Patch
 
sai-jig
sai-jig
diff --git a/CMakeLists.txt b/CMakeLists.txt index 3b08ae7..50e5072 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -195,6 +195,9 @@ if (requirements) if (NOT MSVC AND NOT WIN32) add_subdirectory(src/device) add_subdirectory(src/expect) + if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux") + add_subdirectory(src/jig) + endif() endif() endif() diff --git a/README.md b/README.md index c1be368..bc8bdfd 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,9 @@ accessible by a web interface. push and the revision of `.sai.json` from the new commit in the POST body... the master parses that JSON to fill an sqlite3 database with tasks and commandline options for the build on platforms mentioned in `.sai.json`. + Pushes to branches beginning with `_` are ignored by Sai, even if they have + a valid `.sai.json`; this allows casual sharing of trees via the same git + repo during intense development without continually triggering CI builds. - Builders typically build many variations of the same push, so they use a local git mirror only on the builder to reduce the load on the repo that @@ -64,6 +67,18 @@ accessible by a web interface. independent platform build, and multiple platform builds (eg, cross toolchains). + - Embedded test devices that need management by external gpios to select + flash or test modes can be wired to an RPi or similar running sai-jig. + This listens on a configurable port for requests to perform gpio sequencing + specified in a configuration file. One sai-jig instance can separately + manage external gpio sequencing for multiple test targets. + + - Largely the master is automatic, driven by git hook notifications over + HTTP and the UI is read-only. However there are some privileged UI operations + like deleting a whole event, or redoing whole events or individual tasks. + For these, if the browser has an authentic JWT signed by the master, it can + see and operate these privileged controls. + ## Build flow and support for embedded ![build flow](READMEs/sai-build-test-flow.png) @@ -90,6 +105,8 @@ requested from inside the build action by another tool built with `sai-builder`, `sai-device`, which reads shared JSON config describing the available devices and platforms they are appropriate for. +![sai-device overview](READMEs/sai-embedded-test.png) + Rather than reserve the device when the build is spawned, the reservation needs to happen only when the build inside the build context has completed. That in turn means that a different sai utility has to run at that time from @@ -156,9 +173,9 @@ variables to the child build and test process as follows Environment Var|Ch#|Meaning|Example ---|---|---|--- -SAI_LOGPROXY|3|Build progress logging|@com.warmcat.com.saib.logproxy.warmcat_com-freertos-esp32.0 -SAI_LOGPROXY_TTY0|4|Device tty0|@com.warmcat.com.saib.logproxy.warmcat_com-freertos-esp32.0.tty0 -SAI_LOGPROXY_TTY1|5|Optional Device tty1|@com.warmcat.com.saib.logproxy.warmcat_com-freertos-esp32.0.tty1 +SAI_LOGPROXY|3|Build progress logging|`@com.warmcat.com.saib.logproxy.warmcat_com-freertos-esp32.0` +SAI_LOGPROXY_TTY0|4|Device tty0|`@com.warmcat.com.saib.logproxy.warmcat_com-freertos-esp32.0.tty0` +SAI_LOGPROXY_TTY1|5|Optional Device tty1|`@com.warmcat.com.saib.logproxy.warmcat_com-freertos-esp32.0.tty1` Because some kinds of device share the same tty for flashing the device, at which time nothing else must be reading from the tty, tty activity is only @@ -257,7 +274,7 @@ For redhat type distros, you probably need to add /usr/local/lib to the ``` $ git clone https://libwebsockets.org/repo/libwebsockets $ cd libwebsockets && mkdir build && cd build && \ - cmake .. -DLWS_UNIX_SOCK=1 -DLWS_WITH_STRUCT_JSON=1 \ + cmake .. -DLWS_UNIX_SOCK=1 -DLWS_WITH_STRUCT_JSON=1 -DLWS_WITH_JOSE=1 \ -DLWS_WITH_STRUCT_SQLITE3=1 -DLWS_WITH_GENCRYPTO=1 -DLWS_WITH_SPAWN=1 \ -DLWS_WITH_SECURE_STREAMS=1 -DLWS_WITH_THREADPOOL=1 $ make -j && sudo make -j install && sudo ldconfig @@ -269,8 +286,8 @@ sai-builder. Feature|lws options ---|--- either|`-DLWS_WITH_STRUCT_JSON=1` `-DLWS_WITH_SECURE_STREAMS=1` -master|`-DLWS_UNIX_SOCK=1` `-DLWS_WITH_GENCRYPTO=1` `-DLWS_WITH_STRUCT_SQLITE3=1` -builder|`-DLWS_WITH_SPAWN=1` `-DLWS_WITH_THREADPOOL=1` +master|`-DLWS_UNIX_SOCK=1` `-DLWS_WITH_GENCRYPTO=1` `-DLWS_WITH_STRUCT_SQLITE3=1` `-DLWS_WITH_JOSE=1` +builder + related|`-DLWS_WITH_SPAWN=1` `-DLWS_WITH_THREADPOOL=1` Similarly the two daemons bring in different dependencies @@ -279,6 +296,7 @@ Feature|dependency either|libwebsockets master|libsqlite3 builder|libgit2 pthreads +jig|libgpiod #### Linux diff --git a/READMEs/README-auth.md b/READMEs/README-auth.md new file mode 100644 index 0000000..42f4372 --- /dev/null +++ b/READMEs/README-auth.md @@ -0,0 +1,60 @@ +# Sai web auth + +## Authorization Overview + +Sai uses signed JWTs + +There's no UI at the moment for creating authorized users, everything except +event deletion and task restart works without authorization. + +## sai-master configuration for auth + +In the `vhosts|ws-protocols|com-warmcat-sai` section of the config JSON, the +following entries define the authorization operation + +``` + "jwt-auth-alg": "ES512", + "jwt-auth-jwk-path": "/etc/sai/master/auth.jwk", + "jwt-iss": "com.warmcat", + "jwt-aud": "https://libwebsockets.org/sai", +``` + +The `jwt-iss` and `jwt-aud` values go into generated JWTs and are confirmed +to match when receiving a JWT, these define the issuing authority and the +"audience", the receipient the JWT was created to be consumed by... the +audience should be the globally unique site base URI. + +## Creating the server JWK + +If you build lws with +`-DLWS_WITH_GENCRYPTO=1 -DLWS_WITH_JOSE=1 -DLWS_WITH_MINIMAL_EXAMPLES=1` +it will create a set of JOSE utilities, including one for JWK key generation. + +Use this as below to create the server JWK used for signing and validation, +for ES512 algorithm in our case + +``` +$ sudo ./bin/lws-crypto-jwk -t EC -b512 -vP-521 --alg ES512 > /etc/sai/master/auth.jwk +``` + +## Defining an authorized user + +Sai-master creates a separate auth database and prepares the table schema in +it on startup if not already existing. So you using sai-master at all already +did most of the work. + +To create a user that can login via his browser and see and use the UI for the +additional actions, currently you can create the user by hand on the server: + +``` +# sqlite3 /home/srv/sai/sai-master-auth.sqlite3 +SQLite version 3.32.3 2020-06-18 14:00:33 +Enter ".help" for usage hints. +sqlite> .schema +CREATE TABLE auth (_lws_idx integer, name varchar, passphrase varchar, since integer primary key autoincrement, last_updated integer); +CREATE TABLE sqlite_sequence(name,seq); +sqlite> insert into auth (_lws_idx, name, passphrase) values (0, "your@email.com", "somepassword"); +``` + +Afterwards, it should be possible to log in from the web UI using the given +credentials and see the additional UI elements. diff --git a/READMEs/README-sai-jig.md b/READMEs/README-sai-jig.md new file mode 100644 index 0000000..86f7a2c --- /dev/null +++ b/READMEs/README-sai-jig.md @@ -0,0 +1,99 @@ +# Sai-jig + +## Introduction + +Sai-jig is a standalone daemon that normally doesn't run on the builder or +master machines, but on a smaller helper close to embedded targets and +physically wired to, eg, the reset button or flash config GPIO on them. +The helper is typically an RPi or similar machine that has networking, +Linux and convenient GPIO. + +Sai-jig lets you formally describe the gpio and sequences involving it using +static JSON config on the helper machine, then opens an HTTP server on a +configured interface and port for the builders to request management of DUT +embedded boards using the sequences defined by name in the config, as part of +the CTest flow for the device. + +The configuration supports multiple targets each with their own gpio namespace, +so one helper board can manage many DUTs each using their own gpio. + +## Configuration example + +``` +{ + "schema": "sai-jig", + "port": 44000, + "targets": [ + { + "name": "linkit-7697-1", + "gpios": [ + { + "chip_index": 0, + "name": "nReset", + "offset": 17, + "wire": "RST", + "safe": 0 + }, { + "name": "usr", + "chip_index": 0, + "offset": 22, + "wire": "P6", + "safe": 0 + } + ], "sequences": [ + { + "name": "reset", + "seq": [ + { "gpio_name": "nReset", "value": 0 }, + { "gpio_name": "usr", "value": 0 }, + { "value": 300 }, + { "gpio_name": "nReset", "value": 1 } + ] + }, { + "name": "flash", + "seq": [ + { "gpio_name": "nReset", "value": 0 }, + { "gpio_name": "usr", "value": 1 }, + { "value": 300 }, + { "gpio_name": "nReset", "value": 1 }, + { "value": 100 }, + { "gpio_name": "usr", "value": 0 } + ] + } + ] + } + ] +} +``` + +JSON elements + +Name|Meaning +---|--- +`schema`|Must be `sai-jig` +`port`|Which port number to listen on +`iface`|Optional, which network interface to bind the listen port to +`targets`|All remaining config is specific to each target listed here +`targets/name`|The name of the target, used by remote clients +`targets/gpios`|List of gpios used by the target +`targets/gpios/name`|Name the target uses for this gpio +`targets/gpios/chip_index`|The libgpiod / linux gpiochip index, usually 0 +`targets/gpios/offset`|The libgpiod gpio index inside the chip, usually "the gpio number" like 17 +`targets/gpios/wire`|String documenting where the gpio is wired to on the target, not used by sai-jig +`targets/gpios/safe`|The "safe" level to init the gpio to, 0 or 1 +`targets/sequences`|Describes named sequences of GPIO activity the remote client can ask to run +`targets/sequences/name`|The sequence name +`targets/sequences/seq`|A list of gpio actions done by the sequence +`targets/sequences/seq/gpio_name`|The gpio name changes as part of the sequence, indicates `value` is a time in ms if absent +`targets/sequences/seq/value`|0 or 1 to set the named gpio, or if that is absent, how many ms to pause before doing the next step + +## Triggering sequences + +sai-jig runs a normal HTTP server on the requested port bound to the requested +interface. You can use normal HTTP tool like `curl` to trigger sequences on +specific targets, the HTTP request returns with a 200 when the sequence completes, +so it's easy to synchronize even though the requestor may be on a different machine. + +The transaction should be an HTTP GET to `/target-name/sequence-name`, eg with the +example config above, `curl http://myhelperip:myport/linkit-7697-1/flash`. + diff --git a/READMEs/sai-embedded-test.png b/READMEs/sai-embedded-test.png index e617c07..03e9c70 100644 Binary files a/READMEs/sai-embedded-test.png and b/READMEs/sai-embedded-test.png differ diff --git a/READMEs/sai-overview.png b/READMEs/sai-overview.png index 1577021..069460d 100644 Binary files a/READMEs/sai-overview.png and b/READMEs/sai-overview.png differ diff --git a/assets/index.html b/assets/index.html index 1125093..20f099f 100644 --- a/assets/index.html +++ b/assets/index.html @@ -9,21 +9,37 @@ </head> <body> <div class="summary nailed"> - <table><tr> - <td class="logo"> - <img src="sai.svg"> - </td> - <td class="builder"> - <div id="sai_builders"></div> - </td> - </tr> - <tr><td colspan="2"><div id="sai_sticky"></div></td></tr> - </table> - </div> - <div id="p2" class="undernailed"> + <div class="logo"> + <img class="logo" src="sai.svg"> + <a href="https://warmcat.com/git/sai">Sai git</a> + </div> + + <div id="login" class="login"> + <div id="creds" class="creds hide"> + <form class="login" action="login" method="post"> + <label for="lname">Name:</label> + <input class="crin" type="text" id="lname" name="lname" autocomplete="username"><br> + <label for="lpass">Password:</label> + <input class="crin" type="password" id="lpass" name="lpass" autocomplete="current-password"><br> + <input class="crinb" type="submit" value="Login"> + <input type="hidden" id="success_redir" name="success_redir" value=""> + </form> + </div> + <div id="logout" class="creds hide"> + <div id="remauth"></div> + <input class="crin" type="submit" id="logout" value="Logout"> + </div> + </div> + + <div class="builder"> + <div id="sai_builders"></div> + </div> + </div> + <div id="sai_sticky" class="sticky"></div> + <div id="p2" class="undernailed"> - <td colspan=2 class="summary" id="summary"> + <td colspan=3 class="summary" id="summary"> <noscript><span id="noscript" class="noscript">Please enable Javascript</span></noscript> <div id="sai_overview"></div> <div id="sai_task"></div> diff --git a/assets/sai.css b/assets/sai.css index f505808..2f6f714 100644 --- a/assets/sai.css +++ b/assets/sai.css @@ -197,14 +197,15 @@ div.taskstate { div.nailed { position:fixed; background: #fff; - width:100%; + width:auto; z-index:1001; margin-top:-12px; } div.undernailed { position:relative; - top: 180px; + top: 0px; + margin-left: 180px; } @@ -380,6 +381,46 @@ div.dlogst { } +div.hide { + display: none; +} + +input.crin { + font-size: 7pt; + width: 60px; +} + +input.crinb { + font-size: 7pt; + margin-top: 4px; +} + +label { + float: left; + width: 4.5em; + margin-right: 1em; +} + +form.login { + width: auto; + color:#808080; + text-align: right; +} + +div.login { + display: block; + vertical-align: middle; + font-size: 7pt; + color:#3d9970; + text-align:left; + margin: 0px 16px 0px 16px; + padding: 2px 3px; + border-radius:5px; + float: left; + width: auto; +} + + div.sai_arts { display: inline-flex; vertical-align: middle; @@ -395,7 +436,7 @@ div.sai_arts { span.ti1 { font-weight: normal; font-size: 9pt; - background:#d0c0b0; + //background:#d0c0b0; color:#000000; padding:3px; border-radius:3px; @@ -449,29 +490,39 @@ table.summary { margin: 1px; } -table.builders { - height: 100%; - margin-left: 32px; -} - img.bsvg { height: 50px; width: auto; padding:3px; } -td.builder { - width: 99%; +div.builder { + display: block; + position: absolute; + top: 150px; +} + +div.sticky { + margin-left: 180px; } -td.logo { +div.logo { margin: 3px; margin-right: 4px; padding: 2px; vertical-align: top; - z-index: 1000; + z-index: 1002; + width: 164px; + height: auto; + font-size: 6pt; +} + +img.logo { + width: 158px; + height: auto; } + td.summary { background: #f8f8f8; width: 100%; diff --git a/assets/sai.js b/assets/sai.js index 2f523cd..a727411 100644 --- a/assets/sai.js +++ b/assets/sai.js @@ -392,7 +392,12 @@ var lang_zhs = "{" + "\"%{pf}前创建, 创作时间: %{ct}ms \"" + "}}"; -var logs = "", redpend = 0, gitohashi_integ = 0; +var logs = "", redpend = 0, gitohashi_integ = 0, authd = 0, exptimer, auth_user = ""; + +function expiry() +{ + location.reload(); +} function san(s) { @@ -471,11 +476,14 @@ function agify(now, secs) if (!secs) return ""; + + if (secs > now) + d = secs - now; for (n = 0; n < age_names.length; n++) if (d < age_limit[n] || age_limit[n] === 0) return "<span class='age-" + n + "' ut='" + secs + - "'>" + Math.ceil(d / age_div[n]) + + "'>" + ((secs > now) ? "in " : "") + Math.ceil(d / age_div[n]) + i18n(age_names[n]) + "</span>"; } @@ -554,14 +562,15 @@ function sai_taskinfo_render(t, now_ut) s = "<div class=\"taskinfo\"><table><tr class=\"nomar\"><td class=\"atop\"><table>" + sai_event_render(t, now_ut, 0) + "</table></td><td class=\"ti\">" + - "<span class=\"ti1\">" + sai_plat_icon(t.t.platform, 2) + + "<span class=\"taskstate" + t.t.state + " ti1\">" + sai_plat_icon(t.t.platform, 2) + san(t.t.platform) + "</span>&nbsp;"; - if (t.t.state != 0 && t.t.state != 3 && t.t.state != 4 && t.t.state != 5) + if (authd && t.t.state != 0 && t.t.state != 3 && t.t.state != 4 && t.t.state != 5) s += "<img class=\"rebuild\" alt=\"stop build\" src=\"stop.svg\" " + "id=\"stop-" + san(t.t.uuid) + "\">&nbsp;"; - s += "<img class=\"rebuild\" alt=\"rebuild\" src=\"rebuild.png\" " + + if (authd) + s += "<img class=\"rebuild\" alt=\"rebuild\" src=\"rebuild.png\" " + "id=\"rebuild-" + san(t.t.uuid) + "\">&nbsp;" + - sai_stateful_taskname(t.t.state, t.t.taskname, 1); + sai_stateful_taskname(t.t.state, t.t.taskname, 1); if (t.t.builder_name) { var now_ut = Math.round((new Date().getTime() / 1000)); @@ -606,7 +615,7 @@ function sai_event_summary_render(o, now_ut, reset_all_icon) s += "<div class=\"evr\"><img src=\"/sai/failed.svg\"></div>"; s += "</a>"; - if (reset_all_icon && !gitohashi_integ) { + if (reset_all_icon && !gitohashi_integ && authd) { s += "<br><img class=\"rebuild\" alt=\"rebuild all\" src=\"/sai/rebuild.png\" " + "id=\"rebuild-ev-" + san(e.uuid) + "\">&nbsp;"; s += "<img class=\"rebuild\" alt=\"delete event\" src=\"/sai/delete.png\" " + @@ -853,8 +862,11 @@ function ws_open_sai() gitohashi_integ = 1; } - sai = new WebSocket(get_appropriate_ws_url() + "/sai/browse" + s, - "com-warmcat-sai"); + var s1 = get_appropriate_ws_url() + "/sai/browse" + s; + if (s1.split("?")) + s1 = s1.split("?")[0]; + console.log(s1); + sai = new WebSocket(s1, "com-warmcat-sai"); try { sai.onopen = function() { @@ -965,6 +977,30 @@ function ws_open_sai() if (jso.schema == "sai.warmcat.com.overview") { s = "<table>"; + + authd = jso.authorized; + if (jso.authorized === 0) { + if (document.getElementById("creds")) + document.getElementById("creds").classList.remove("hide"); + if (document.getElementById("logout")) + document.getElementById("logout").classList.add("hide"); + } + if (jso.authorized === 1) { + if (document.getElementById("creds")) + document.getElementById("creds").classList.add("hide"); + if (document.getElementById("logout")) + document.getElementById("logout").classList.remove("hide"); + if (jso.auth_user) + auth_user = jso.auth_user; + if (jso.auth_secs) { + var now_ut = Math.round((new Date().getTime() / 1000)); + clearTimeout(exptimer); + exptimer = window.setTimeout(expiry, 1000 * jso.auth_secs); + if (document.getElementById("remauth")) + document.getElementById("remauth").innerHTML = + san(auth_user) + " " + agify(now_ut, now_ut + jso.auth_secs); + } + } if (jso.overview.length) for (n = jso.overview.length - 1; n >= 0; n--) @@ -1006,6 +1042,31 @@ function ws_open_sai() } if (jso.schema == "com.warmcat.sai.taskinfo") { + + authd = jso.authorized; + if (jso.authorized === 0) { + if (document.getElementById("creds")) + document.getElementById("creds").classList.remove("hide"); + if (document.getElementById("logout")) + document.getElementById("logout").classList.add("hide"); + } + if (jso.authorized === 1) { + if (document.getElementById("creds")) + document.getElementById("creds").classList.add("hide"); + if (document.getElementById("logout")) + document.getElementById("logout").classList.remove("hide"); + if (jso.auth_user) + auth_user = jso.auth_user; + if (jso.auth_secs) { + var now_ut = Math.round((new Date().getTime() / 1000)); + clearTimeout(exptimer); + exptimer = window.setTimeout(expiry, 1000 * jso.auth_secs); + if (document.getElementById("remauth")) + document.getElementById("remauth").innerHTML = + san(auth_user) + " " + agify(now_ut, now_ut + jso.auth_secs); + } + } + s = sai_taskinfo_render(jso); if (document.getElementById("sai_sticky")) document.getElementById("sai_sticky").innerHTML = s; @@ -1167,6 +1228,11 @@ window.addEventListener("load", function() { if (document.getElementById("noscript")) document.getElementById("noscript").display = "none"; + + /* login form hidden success redirect */ + if (document.getElementById("success_redir")) + document.getElementById("success_redir").value = + window.location.href; ws_open_sai(); aging(); diff --git a/etc-sai-EXAMPLE/master/conf.d/unixskt b/etc-sai-EXAMPLE/master/conf.d/unixskt index 527045f..2ab6da5 100644 --- a/etc-sai-EXAMPLE/master/conf.d/unixskt +++ b/etc-sai-EXAMPLE/master/conf.d/unixskt @@ -61,20 +61,32 @@ # info for an event goes in its own database file in # ...-event-xxxx.sqlite3 where xxxx is the event uuid. # - "database": "/var/lib/sai/sai-master", - # - # the push hook notification script creates a JSON - # object and signs it with a secret... "notification- - # key" should match the secret the notification hook - # script has that we're willing to accept. - # - # It should be a 64-char hex-ascii representation of - # a random 32-byte sequence, you can produce a strong - # random key in the correct format like this + "database": "/srv/sai/sai-master", + # + # the push hook notification script creates a JSON + # object and signs it with a secret... "notification- + # key" should match the secret the notification hook + # script has that we're willing to accept. + # + # It should be a 64-char hex-ascii representation of + # a random 32-byte sequence, you can produce a strong + # random key in the correct format like this + # + # dd if=/dev/random bs=32 count=1 | sha256sum | cut -d' ' -f1 + # + "notification-key": "1234...5678", + + # auth jwk path + # You can generate a suitable key like this # - # dd if=/dev/random bs=32 count=1 | sha256sum | cut -d' ' -f1 + # lws-crypto-jwk -t EC -b512 -vP-521 --alg ES512 > mykey.jwk # - "notification-key": "xxxxxxxxxxxx", + "jwt-auth-alg": "ES512", + "jwt-auth-jwk-path": "/etc/sai/master/auth.jwk", + + "jwt-iss": "com.warmcat", + "jwt-aud": "https://libwebsockets.org/sai", + # template HTML to use for this vhost. You'd normally # copy this to gitohashi-vhostname.html and modify it # to show the content, logos, links, fonts, css etc for @@ -131,4 +143,3 @@ } ] } - diff --git a/scripts/builder-conf b/scripts/builder-conf index c5ea5c1..eea8f3c 100644 --- a/scripts/builder-conf +++ b/scripts/builder-conf @@ -1,5 +1,8 @@ { "perms": "sai:nobody", +# if you're building RPMs with cpack, you may need to make a path +# here that is longer than the longest rpm filepath you are generating... +# this is a restriction of rpmbuild "home": "/home/sai", "host": "myhostname", diff --git a/scripts/sai-jig.service b/scripts/sai-jig.service new file mode 100644 index 0000000..d41013a --- /dev/null +++ b/scripts/sai-jig.service @@ -0,0 +1,9 @@ +[Unit] +Description=Sai JIG + +[Service] +ExecStart=/usr/local/bin/sai-jig + +[Install] +WantedBy=multi-user.target + diff --git a/src/jig/CMakeLists.txt b/src/jig/CMakeLists.txt new file mode 100644 index 0000000..c668c10 --- /dev/null +++ b/src/jig/CMakeLists.txt @@ -0,0 +1,51 @@ +set(CPACK_DEBIAN_BUILDER_PACKAGE_NAME "sai-jig") + +set(SRCS + j-sai.c + j-conf.c + j-server.c +) + +set(requirements 1) +require_lws_config(LWS_WITH_CLIENT 1 requirements) +require_lws_config(LWS_WITH_UNIX_SOCK 1 requirements) + +if (requirements) + + find_path( GPIOD_INC_PATH NAMES "gpiod.h") + find_library(GPIOD_LIB_PATH NAMES "gpiod") + + if (NOT GPIOD_INC_PATH OR NOT GPIOD_LIB_PATH) + message(FATAL_ERROR " Unable to find libgpiod") + endif() + include_directories(BEFORE "${GPIOD_INC_PATH}") + + add_executable("sai-jig" ${SRCS}) + target_link_libraries("sai-jig" ${GPIOD_LIB_PATH}) + + if (APPLE) + set_property(TARGET sai-device PROPERTY MACOSX_RPATH YES) + endif() + + target_link_libraries("sai-jig" ${SAI_LWS_LIB_PATH}) + + if (LWS_OPENSSL_LIBRARIES) + target_link_libraries("sai-jig" ${LWS_OPENSSL_LIBRARIES}) + endif() + + if (SAI_EXT_PTHREAD_LIBRARIES) + target_link_libraries("sai-jig" ${SAI_EXT_PTHREAD_LIBRARIES}) + endif() + if (HAS_LIBCAP) + target_link_libraries("sai-jig" ${CAP_LIB_PATH}) + endif() + + if (MSVC OR WIN32) + target_link_libraries("sai-jig" ws2_32.lib userenv.lib psapi.lib iphlpapi.lib) + endif() + + install(TARGETS "sai-jig" + RUNTIME DESTINATION "${BIN_DIR}" COMPONENT jig) + +endif(requirements) +include(CPack) diff --git a/src/jig/j-conf.c b/src/jig/j-conf.c new file mode 100644 index 0000000..095abae --- /dev/null +++ b/src/jig/j-conf.c @@ -0,0 +1,228 @@ +/* + * sai-device conf.c + * + * Copyright (C) 2019 - 2020 Andy Green <andy@warmcat.com> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation: + * version 2.1 of the License. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ + +#if defined(WIN32) /* complaints about getenv */ +#define _CRT_SECURE_NO_WARNINGS +#define read _read +#define close _close +#endif + +#include <libwebsockets.h> +#include <string.h> +#include <signal.h> +#include <time.h> +#include <fcntl.h> + +#include "j-private.h" + +/* + * We read the JSON config using lws_struct... instrument the related structures + */ + +static const lws_struct_map_t lsm_sai_jig_gpio[] = { + LSM_UNSIGNED (sai_jig_gpio_t, chip_idx, "chip_idx"), + LSM_UNSIGNED (sai_jig_gpio_t, offset, "offset"), + LSM_UNSIGNED (sai_jig_gpio_t, safe, "safe"), + LSM_STRING_PTR (sai_jig_gpio_t, name, "name"), + LSM_STRING_PTR (sai_jig_gpio_t, wire, "wire"), +}; + +static const lws_struct_map_t lsm_sai_jig_seq_item[] = { + LSM_STRING_PTR (sai_jig_seq_item_t, gpio_name, "gpio_name"), + LSM_UNSIGNED (sai_jig_seq_item_t, value, "value"), +}; + +static const lws_struct_map_t lsm_sai_jig_sequence[] = { + LSM_STRING_PTR (sai_jig_sequence_t, name, "name"), + LSM_LIST (sai_jig_sequence_t, seq_owner, + sai_jig_seq_item_t, list, + NULL, lsm_sai_jig_seq_item, "seq"), +}; + +static const lws_struct_map_t lsm_sai_jig_target[] = { + LSM_STRING_PTR (sai_jig_target_t, name, "name"), + LSM_LIST (sai_jig_target_t, gpio_owner, sai_jig_gpio_t, list, + NULL, lsm_sai_jig_gpio, "gpios"), + LSM_LIST (sai_jig_target_t, seq_owner, sai_jig_sequence_t, list, + NULL, lsm_sai_jig_sequence, "sequences"), +}; + +static const lws_struct_map_t lsm_sai_jig[] = { + LSM_STRING_PTR (sai_jig_t, iface, "iface"), + LSM_UNSIGNED (sai_jig_t, port, "port"), + LSM_LIST (sai_jig_t, target_owner, sai_jig_target_t, list, + NULL, lsm_sai_jig_target, "targets"), +}; + +static const lws_struct_map_t lsm_jig_schema[] = { + LSM_SCHEMA (sai_jig_t, NULL, lsm_sai_jig, "sai-jig"), +}; + +int +saij_config_global(const char *d) +{ + unsigned char buf[128]; + lws_struct_args_t a; + struct lejp_ctx ctx; + int n, m, fd; + + memset(&a, 0, sizeof(a)); + a.map_st[0] = lsm_jig_schema; + a.map_entries_st[0] = LWS_ARRAY_SIZE(lsm_jig_schema); + a.ac_block_size = 512; + +#if defined(WIN32) + lws_snprintf((char *)buf, sizeof(buf) - 1, "%s\\conf", d); +#else + lws_snprintf((char *)buf, sizeof(buf) - 1, "%s/conf", d); +#endif + + fd = lws_open((char *)buf, O_RDONLY); + if (fd < 0) { + lwsl_err("Cannot open %s\n", (char *)buf); + return 2; + } + lwsl_info("%s: %s\n", __func__, (char *)buf); + lws_struct_json_init_parse(&ctx, NULL, &a); + + do { + n = read(fd, buf, sizeof(buf)); + if (!n) + break; + + m = lejp_parse(&ctx, buf, n); + } while (m == LEJP_CONTINUE); + + if (m < 0 || !a.dest) { + lwsl_notice("%s: line %d: JSON decode failed '%s'\n", + __func__, ctx.line, lejp_error_to_string(m)); + goto bail1; + } + + close(fd); + n = ctx.line; + lejp_destruct(&ctx); + + jig = a.dest; + + /* + * Resolve namespace names to objects and find and fail out on any + * inconsistencies. + * + * For each target... + */ + + lws_start_foreach_dll(struct lws_dll2 *, p, jig->target_owner.head) { + sai_jig_target_t *t = + lws_container_of(p, sai_jig_target_t, list); + + /* ... for each gpio in the target */ + + lws_start_foreach_dll(struct lws_dll2 *, z, + t->gpio_owner.head) { + sai_jig_gpio_t *g = lws_container_of(z, sai_jig_gpio_t, + list); + + lwsl_notice("%s: gpio '%s' -> %s (%d:%d)\n", t->name, + g->name, g->wire, g->chip_idx, g->offset); + + if (!jig->chip[g->chip_idx]) { + if (g->chip_idx >= + (int)LWS_ARRAY_SIZE(jig->chip)) { + lwsl_err("%s: chip idx %d too big\n", + __func__, g->chip_idx); + goto bail1; + } + jig->chip[g->chip_idx] = + gpiod_chip_open_by_number(g->chip_idx); + if (!jig->chip[g->chip_idx]) { + lwsl_err("%s: unable to open chip %d\n", + __func__, g->chip_idx); + goto bail1; + } + } + + g->line = gpiod_chip_get_line(jig->chip[g->chip_idx], + g->offset); + if (!g->line) { + lwsl_err("%s: unable to get gpio line %d\n", + __func__, g->offset); + goto bail1; + } + + n = gpiod_line_request_output(g->line, "sai-jig", 0); + if (n) { + lwsl_err("%s: unable to request line %d: %d\n", + __func__, g->offset, errno); + goto bail1; + } + + gpiod_line_set_value(g->line, g->safe); + + } lws_end_foreach_dll(z); + + /* ... list supported target sequences as URLs... */ + + lws_start_foreach_dll(struct lws_dll2 *, q, t->seq_owner.head) { + sai_jig_sequence_t *s = lws_container_of(q, + sai_jig_sequence_t, list); + + lwsl_notice("http://address:%u/%s/%s\n", jig->port, + t->name, s->name); + + /* ... and for each sequence element in the sequence */ + + lws_start_foreach_dll(struct lws_dll2 *, w, + s->seq_owner.head) { + sai_jig_seq_item_t *i = lws_container_of(w, + sai_jig_seq_item_t, list); + + if (i->gpio_name) { + i->gpio = lws_dll2_search_sz_pl( + &t->gpio_owner, i->gpio_name, + strlen(i->gpio_name), + sai_jig_gpio_t, list, name); + if (!i->gpio) { + lwsl_err("%s: %s unknown\n", + __func__, i->gpio_name); + } + } + + } lws_end_foreach_dll(w); + + } lws_end_foreach_dll(q); + + } lws_end_foreach_dll(p); + + return 0; + +bail1: + lwsac_free(&a.ac); + + return 1; +} + +void +saij_config_destroy(sai_jig_t **jig) +{ + lwsac_free(&(*jig)->ac_conf); + *jig = NULL; +} diff --git a/src/jig/j-private.h b/src/jig/j-private.h new file mode 100644 index 0000000..1d25533 --- /dev/null +++ b/src/jig/j-private.h @@ -0,0 +1,78 @@ +/* + * sai-jig private + * + * Copyright (C) 2019 - 2020 Andy Green <andy@warmcat.com> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation: + * version 2.1 of the License. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ + +#include <gpiod.h> + +typedef struct { + lws_dll2_t list; + + struct gpiod_line *line; + + const char *name; + const char *wire; + + int chip_idx; + int offset; + int safe; +} sai_jig_gpio_t; + +typedef struct { + lws_dll2_t list; + sai_jig_gpio_t *gpio; /* null = wait ms */ + const char *gpio_name; + int value; +} sai_jig_seq_item_t; + +typedef struct { + lws_dll2_t list; + lws_dll2_owner_t seq_owner; + const char *name; +} sai_jig_sequence_t; + +typedef struct { + lws_dll2_t list; + lws_dll2_owner_t gpio_owner; + lws_dll2_owner_t seq_owner; + + lws_sorted_usec_list_t sul; /* next step in ongoing seq */ + sai_jig_seq_item_t *current; /* next seq step */ + + const char *name; + + struct lws *wsi; +} sai_jig_target_t; + +typedef struct { + lws_dll2_owner_t target_owner; + struct gpiod_chip *chip[16]; + struct lwsac *ac_conf; + int port; + const char *iface; + struct lws_context *ctx; +} sai_jig_t; + +extern sai_jig_t *jig; + +int +saij_config_global(const char *d); + +void +saij_config_destroy(sai_jig_t **jig); diff --git a/src/jig/j-sai.c b/src/jig/j-sai.c new file mode 100644 index 0000000..0314be2 --- /dev/null +++ b/src/jig/j-sai.c @@ -0,0 +1,151 @@ +/* + * sai-jig + * + * Copyright (C) 2019 - 2020 Andy Green <andy@warmcat.com> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation: + * version 2.1 of the License. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + * + * + * sai-jig is usually running on something like an RPi rather than the + * big build machine... it is told via its JSON config how its GPIO are + * wired to control other devices known to sai-device, and exposes control + * of those over a local http server that can be written from inside the + * build machine CTest flow. + */ + +#if defined(WIN32) /* complaints about getenv */ +#define _CRT_SECURE_NO_WARNINGS +#endif + +#include <libwebsockets.h> +#include <string.h> +#include <signal.h> +#include <fcntl.h> + +#if defined(WIN32) +#include <initguid.h> +#include <KnownFolders.h> +#include <Shlobj.h> +#else +#include <sys/file.h> +#include <unistd.h> +#endif + +#if defined(__linux__) +#include <sys/prctl.h> +#endif + +#include "j-private.h" + +static const char *config_dir = "/etc/sai/jig"; +static int interrupted; +sai_jig_t *jig; + +extern const struct lws_protocols *pprotocols[]; + +void sigint_handler(int sig) +{ + interrupted = 1; +} + +int main(int argc, const char **argv) +{ + int logs = LLL_USER | LLL_ERR | LLL_WARN | LLL_NOTICE; + struct lws_context_creation_info info; +#if defined(WIN32) + char temp[256], stg_config_dir[256]; +#endif + const char *p; + + if ((p = lws_cmdline_option(argc, argv, "-d"))) + logs = atoi(p); + + lws_set_log_level(logs, NULL); + +#if defined(WIN32) + { + PWSTR wdi = NULL; + + if (SHGetKnownFolderPath(&FOLDERID_ProgramData, + 0, NULL, &wdi) != S_OK) { + lwsl_err("%s: unable to get config dir\n", __func__); + return 1; + } + + if (WideCharToMultiByte(CP_ACP, 0, wdi, -1, temp, + sizeof(temp), 0, NULL) <= 0) { + lwsl_err("%s: problem with string encoding\n", __func__); + return 1; + } + + lws_snprintf(stg_config_dir, sizeof(stg_config_dir), + "%s\\sai\\jig\\", temp); + + config_dir = stg_config_dir; + CoTaskMemFree(wdi); + } +#endif + + lwsl_notice("Sai Jig - " + "Copyright (C) 2019-2020 Andy Green <andy@warmcat.com>\n"); + + /* + * Let's parse the global bits out of the config + */ + + lwsl_info("%s: config dir %s\n", __func__, config_dir); + if (saij_config_global(config_dir)) { + lwsl_err("%s: global config failed\n", __func__); + + return 1; + } + + memset(&info, 0, sizeof info); + info.port = jig->port; + info.iface = jig->iface; + info.pt_serv_buf_size = 4 * 1024; + info.pprotocols = pprotocols; + info.options = LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT | + LWS_SERVER_OPTION_VALIDATE_UTF8 | + LWS_SERVER_OPTION_EXPLICIT_VHOSTS; + + signal(SIGINT, sigint_handler); + info.fd_limit_per_thread = 1 + 16 + 1; + + /* create the lws context */ + + jig->ctx = lws_create_context(&info); + if (!jig->ctx) { + lwsl_err("lws init failed\n"); + return 1; + } + + /* ... and our vhost... */ + + if (!lws_create_vhost(jig->ctx, &info)) { + lwsl_err("Failed to create tls vhost\n"); + goto bail; + } + + while (!lws_service(jig->ctx, 0) && !interrupted) + ; + +bail: + lws_context_destroy(jig->ctx); + saij_config_destroy(&jig); + + return 0; +} diff --git a/src/jig/j-server.c b/src/jig/j-server.c new file mode 100644 index 0000000..4040288 --- /dev/null +++ b/src/jig/j-server.c @@ -0,0 +1,221 @@ +/* + * sai-jig server + * + * Copyright (C) 2019 - 2020 Andy Green <andy@warmcat.com> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation: + * version 2.1 of the License. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + * + * This is the http server part of sai-jig + */ + +#include <libwebsockets.h> +#include <string.h> +#include <signal.h> +#include <time.h> + +#include "j-private.h" + +struct pss { + struct lws_context *ctx; + sai_jig_target_t *target; + struct lws *wsi; + int ret; +}; + +/* + * Perform the next step in the current sequence, be it gpio setting, a wait, + * or completion. If the wsi that triggered it is still around, send the http + * response on completion. + */ + +void +sai_jig_cb(lws_sorted_usec_list_t *sul) +{ + sai_jig_target_t *t = lws_container_of(sul, sai_jig_target_t, sul); + + while (t->current) { + char set = 0; + + if (t->current->gpio) { + gpiod_line_set_value(t->current->gpio->line, + t->current->value); + lwsl_notice("%s: %s <- %d\n", __func__, + t->current->gpio_name, t->current->value); + } else { + /* it's a delay */ + lws_sul_schedule(jig->ctx, 0, &t->sul, sai_jig_cb, + t->current->value * LWS_US_PER_MS); + lwsl_notice("%s: wait %dms\n", __func__, + t->current->value); + set = 1; + } + + if (t->current->list.next) + t->current = lws_container_of(t->current->list.next, + sai_jig_seq_item_t, list); + else + t->current = NULL; + + if (set) + /* we are coming back */ + return; + } + + /* We just finished the sequence */ + + lwsl_notice("%s: sequence finished\n", __func__); + + if (t->wsi) { + struct pss *pss = (struct pss *)lws_wsi_user(t->wsi); + + /* + * the wsi is still around + */ + + pss->ret = 200; + lws_callback_on_writable(t->wsi); + } + + t->wsi = NULL; +} + +static int +callback_dynamic_http(struct lws *wsi, enum lws_callback_reasons reason, + void *user, void *in, size_t len) +{ + struct pss *pss = (struct pss *)user; + uint8_t buf[LWS_PRE + 2048], *start = &buf[LWS_PRE], *p = start, + *end = &buf[sizeof(buf) - LWS_PRE - 1]; + sai_jig_sequence_t *seq; + struct lws_tokenize ts; + + switch (reason) { + case LWS_CALLBACK_HTTP: + + /* + * We want a url like /targetname/sequencename... eg, + * /linkit-7697-1/reset ... if the target is busy we will + * respond with a 400, unknown a 404, otherwise we start the + * sequence and will do a 200 at the end. + */ + + pss->ctx = lws_get_context(wsi); + pss->target = NULL; + lws_tokenize_init(&ts, (const char *)in, + LWS_TOKENIZE_F_NO_INTEGERS | + LWS_TOKENIZE_F_DOT_NONTERM | + LWS_TOKENIZE_F_MINUS_NONTERM); + ts.len = len; + do { + ts.e = lws_tokenize(&ts); + switch (ts.e) { + case LWS_TOKZE_TOKEN: + if (!pss->target) { + + /* + * This is supposed to be the target + * name then... + */ + + pss->target = lws_dll2_search_sz_pl( + &jig->target_owner, ts.token, + ts.token_len, sai_jig_target_t, + list, name); + if (!pss->target) { + pss->ret = HTTP_STATUS_NOT_FOUND; + goto fin; + } + if (pss->target->current) { + pss->ret = HTTP_STATUS_CONFLICT; + goto fin; + } + break; + } + + /* + * this is the sequence name then... + */ + + seq = lws_dll2_search_sz_pl( + &pss->target->seq_owner, ts.token, + ts.token_len, sai_jig_sequence_t, + list, name); + if (!seq) { + pss->ret = HTTP_STATUS_BAD_REQUEST; + goto fin; + } + + /* + * It seems we can do it. + * + * Start with the first sequence element... + */ + + pss->target->current = lws_container_of( + seq->seq_owner.head, + sai_jig_seq_item_t, list); + pss->target->wsi = wsi; + + lws_sul_schedule(pss->ctx, 0, &pss->target->sul, + sai_jig_cb, 1); + + lws_get_peer_simple(wsi, (char *)buf, + sizeof(buf)); + lwsl_notice("%s: Starting %s seq %s\n", buf, + pss->target->name, seq->name); + + return 0; + + case LWS_TOKZE_DELIMITER: + break; + case LWS_TOKZE_ENDED: + pss->ret = HTTP_STATUS_BAD_REQUEST; + goto fin; + } + } while (ts.e > 0); + + return -1; + + case LWS_CALLBACK_CLOSED_HTTP: + if (pss->target) + pss->target->wsi = NULL; + break; + + case LWS_CALLBACK_HTTP_WRITEABLE: + + if (!pss || !pss->ret) + break; + +fin: + if (lws_add_http_common_headers(wsi, pss->ret, "text/html", 0, + &p, end)) + return 1; + if (lws_finalize_write_http_header(wsi, start, &p, end)) + return 1; + + return -1; + + default: + break; + } + + return lws_callback_http_dummy(wsi, reason, user, in, len); +} + +static const struct lws_protocols protocol = + { "http", callback_dynamic_http, sizeof(struct pss), 0 }; + +const struct lws_protocols *pprotocols[] = { &protocol, NULL }; diff --git a/src/master/m-comms.c b/src/master/m-comms.c index 3a8eef8..be88900 100644 --- a/src/master/m-comms.c +++ b/src/master/m-comms.c @@ -32,6 +32,8 @@ #include <string.h> #include <signal.h> #include <time.h> +#include <stdio.h> +#include <fcntl.h> #include "m-private.h" @@ -60,8 +62,26 @@ const lws_struct_map_t lsm_schema_map_ta[] = { LSM_SCHEMA (sai_task_t, NULL, lsm_task, "com-warmcat-sai-ta"), }; -extern const lws_struct_map_t lsm_schema_sq3_map_event[]; +typedef struct sai_auth { + lws_dll2_t list; + char name[33]; + char passphrase[65]; + unsigned long since; + unsigned long last_updated; +} sai_auth_t; + +const lws_struct_map_t lsm_auth[] = { + LSM_CARRAY (sai_auth_t, name, "name"), + LSM_CARRAY (sai_auth_t, passphrase, "passphrase"), + LSM_UNSIGNED (sai_auth_t, since, "since"), + LSM_UNSIGNED (sai_auth_t, last_updated, "last_updated"), +}; +const lws_struct_map_t lsm_schema_sq3_map_auth[] = { + LSM_SCHEMA_DLL2 (sai_auth_t, list, NULL, lsm_auth, "auth"), +}; + +extern const lws_struct_map_t lsm_schema_sq3_map_event[]; static int sai_destroy_builder(struct lws_dll2 *d, void *user) @@ -81,8 +101,6 @@ saim_master_destroy(saim_t *master) lws_struct_sq3_close(&master->pdb); } -static char *hexch = "0123456789abcdef"; - /* len is typically 16 (event uuid is 32 chars + NUL) * But eg, task uuid is concatenated 32-char eventid and 32-char taskid */ @@ -90,20 +108,7 @@ static char *hexch = "0123456789abcdef"; int sai_uuid16_create(struct lws_context *context, char *dest33) { - uint8_t *r = ((uint8_t *)dest33) + 33 - 16; - size_t n = 16; - - if (lws_get_random(context, r, n) != n) - return 1; - - while (n--) { - *dest33++ = hexch[(*r) >> 4]; - *dest33++ = hexch[(*r++) & 0xf]; - } - - *dest33 = '\0'; - - return 0; + return lws_hex_random(context, dest33, 33); } int @@ -269,13 +274,15 @@ typedef enum { SHMUT_BROWSE, SHMUT_STATUS, SHMUT_ARTIFACTS, + SHMUT_LOGIN } sai_http_murl_t; static const char * const well_known[] = { "/update-hook", "/sai/browse", "/status", - "/artifacts/" /* HTTP api for accessing build artifacts */ + "/artifacts/", /* HTTP api for accessing build artifacts */ + "/login" }; static const char *hmac_names[] = { @@ -327,18 +334,39 @@ sai_get_head_status(struct vhd *vhd, const char *projname) static int +sai_login_cb(void *data, const char *name, const char *filename, + char *buf, int len, enum lws_spa_fileupload_states state) +{ + return 0; +} + +static const char * const auth_param_names[] = { + "lname", + "lpass", + "success_redir", +}; + +enum enum_param_names { + EPN_LNAME, + EPN_LPASS, + EPN_SUCCESS_REDIR, +}; + +static int callback_ws(struct lws *wsi, enum lws_callback_reasons reason, void *user, void *in, size_t len) { struct vhd *vhd = (struct vhd *)lws_protocol_vh_priv_get( lws_get_vhost(wsi), lws_get_protocol(wsi)); - uint8_t buf[LWS_PRE + 6144], *start = &buf[LWS_PRE], *p = start, + uint8_t buf[LWS_PRE + 8192], *start = &buf[LWS_PRE], *p = start, *end = &buf[sizeof(buf) - LWS_PRE - 1]; struct pss *pss = (struct pss *)user; + struct lws_jwt_sign_set_cookie ck; sai_http_murl_t mu = SHMUT_NONE; char projname[64]; int n, resp, r; const char *cp; + size_t cml; (void)end; (void)p; @@ -386,6 +414,85 @@ callback_ws(struct lws *wsi, enum lws_callback_reasons reason, void *user, return -1; } + /* auth database */ + + lws_snprintf((char *)buf, sizeof(buf), "%s-auth.sqlite3", + vhd->sqlite3_path_lhs); + + if (lws_struct_sq3_open(vhd->context, (char *)buf, 1, + &vhd->master.pdb_auth)) { + lwsl_err("%s: Unable to open auth db %s: %s\n", + __func__, vhd->sqlite3_path_lhs, sqlite3_errmsg( + vhd->master.pdb)); + + return -1; + } + + if (lws_struct_sq3_create_table(vhd->master.pdb_auth, + lsm_schema_sq3_map_auth)) { + lwsl_err("%s: unable to create auth table\n", __func__); + return -1; + } + + /* + * jwt-iss + */ + + if (lws_pvo_get_str(in, "jwt-iss", &vhd->jwt_issuer)) { + lwsl_err("%s: jwt-iss required\n", __func__); + return -1; + } + + /* + * jwt-aud + */ + + if (lws_pvo_get_str(in, "jwt-aud", &vhd->jwt_audience)) { + lwsl_err("%s: jwt-aud required\n", __func__); + return -1; + } + + /* + * auth-alg + */ + + if (lws_pvo_get_str(in, "jwt-auth-alg", &cp)) { + lwsl_err("%s: jwt-auth-alg required\n", __func__); + return -1; + } + + lws_strncpy(vhd->jwt_auth_alg, cp, sizeof(vhd->jwt_auth_alg)); + + /* + * auth-jwk-path + */ + + if (lws_pvo_get_str(in, "jwt-auth-jwk-path", &cp)) { + lwsl_err("%s: jwt-auth-jwk-path required\n", __func__); + return -1; + } + + n = open(cp, LWS_O_RDONLY); + if (!n) { + lwsl_err("%s: can't open auth JWK %s\n", __func__, cp); + return -1; + } + r = read(n, buf, sizeof(buf)); + close(n); + if (r < 0) { + lwsl_err("%s: can't read auth JWK %s\n", __func__, cp); + return -1; + } + + if (lws_jwk_import(&vhd->jwt_jwk_auth, NULL, NULL, + (const char *)buf, r)) { + lwsl_notice("%s: Failed to parse JWK key\n", __func__); + return -1; + } + + lwsl_notice("%s: Auth JWK type %d\n", __func__, + vhd->jwt_jwk_auth.kty); + lws_sul_schedule(vhd->context, 0, &vhd->sul_central, saim_central_cb, 500 * LWS_US_PER_MS); @@ -404,6 +511,33 @@ callback_ws(struct lws *wsi, enum lws_callback_reasons reason, void *user, resp = HTTP_STATUS_FORBIDDEN; pss->vhd = vhd; + /* + * What's the situation with a JWT cookie? Normal users won't + * have any, but privileged users will have one, and we should + * try to confirm it and set the pss auth level accordingly + */ + + memset(&ck, 0, sizeof(ck)); + ck.jwk = &vhd->jwt_jwk_auth; + ck.alg = vhd->jwt_auth_alg; + ck.iss = vhd->jwt_issuer; + ck.aud = vhd->jwt_audience; + ck.cookie_name = "__Host-sai_jwt"; + + cml = sizeof(buf); + if (!lws_jwt_get_http_cookie_validate_jwt(wsi, &ck, + (char *)buf, &cml) && + ck.extra_json && + !lws_json_simple_strcmp(ck.extra_json, ck.extra_json_len, + "\"authorized\":", "1")) { + /* the token allows him to manage us */ + pss->authorized = 1; + pss->expiry_unix_time = ck.expiry_unix_time; + lws_strncpy(pss->auth_user, ck.sub, + sizeof(pss->auth_user)); + } else + lwsl_err("%s: cookie rejected\n", __func__); + for (n = 0; n < (int)LWS_ARRAY_SIZE(well_known); n++) if (!strncmp((const char *)in, well_known[n], strlen(well_known[n]))) { @@ -452,6 +586,11 @@ callback_ws(struct lws *wsi, enum lws_callback_reasons reason, void *user, goto passthru; + case SHMUT_LOGIN: + pss->login_form = 1; + lwsl_notice("LWS_CALLBACK_HTTP: sees login\n"); + return 0; + case SHMUT_ARTIFACTS: /* * HTTP Bulk GET interface for artifact download @@ -548,6 +687,22 @@ http_resp: case LWS_CALLBACK_HTTP_BODY: + if (pss->login_form) { + + if (!pss->spa) { + pss->spa = lws_spa_create(wsi, auth_param_names, + LWS_ARRAY_SIZE(auth_param_names), + 1024, sai_login_cb, pss); + if (!pss->spa) { + lwsl_err("failed to create spa\n"); + return -1; + } + } + + goto spa_process; + + } + if (!pss->our_form) { lwsl_notice("%s: not our form\n", __func__); goto passthru; @@ -610,6 +765,8 @@ http_resp: } } +spa_process: + /* let it parse the POST data */ if (!pss->spa_failed && @@ -626,14 +783,132 @@ http_resp: lwsl_user("%s: LWS_CALLBACK_HTTP_BODY_COMPLETION: %d\n", __func__, (int)len); - if (!pss->our_form) { + if (!pss->our_form && !pss->login_form) { lwsl_user("%s: no sai form\n", __func__); goto passthru; } - /* inform the spa no more payload data coming */ - lws_spa_finalize(pss->spa); + if (pss->spa) + lws_spa_finalize(pss->spa); + + if (pss->login_form) { + const char *un, *pw, *sr; + lws_dll2_owner_t o; + struct lwsac *ac = NULL; + + if (lws_add_http_header_status(wsi, + HTTP_STATUS_SEE_OTHER, &p, end)) + goto clean_spa; + if (lws_add_http_header_content_length(wsi, 0, &p, end)) + goto clean_spa; + + if (pss->spa_failed) + goto final; + + un = lws_spa_get_string(pss->spa, EPN_LNAME); + pw = lws_spa_get_string(pss->spa, EPN_LPASS); + sr = lws_spa_get_string(pss->spa, EPN_SUCCESS_REDIR); + + if (!un || !pw || !sr) { + pss->spa_failed = 1; + goto final; + } + + lwsl_notice("%s: login attempt %s %s %s\n", + __func__, un, pw, sr); + + /* + * Try to look up his credentials + */ + + lws_sql_purify((char *)buf + 512, un, 34); + lws_sql_purify((char *)buf + 768, pw, 66); + lws_snprintf((char *)buf + 256, 256, + " and name='%s' and passphrase='%s'", + (const char *)buf + 512, + (const char *)buf + 768); + lws_dll2_owner_clear(&o); + n = lws_struct_sq3_deserialize(pss->vhd->master.pdb_auth, + (const char *)buf + 256, + NULL, + lsm_schema_sq3_map_auth, + &o, &ac, 0, 1); + if (n < 0 || !o.head) { + /* no results, failed */ + lwsl_notice("%s: login attempt %s failed %d\n", + __func__, (const char *)buf, n); + lwsac_free(&ac); + pss->spa_failed = 1; + goto final; + } + + /* any result in o means a successful match */ + + lwsac_free(&ac); + + /* + * Produce a signed JWT allowing managing this Sai + * instance for a short time, and redirect ourselves + * back to the page we were on + */ + + + + lwsl_notice("%s: setting cookie\n", __func__); + /* un is invalidated by destroying the spa */ + memset(&ck, 0, sizeof(ck)); + lws_strncpy(ck.sub, un, sizeof(ck.sub)); + ck.jwk = &vhd->jwt_jwk_auth; + ck.alg = vhd->jwt_auth_alg; + ck.iss = vhd->jwt_issuer; + ck.aud = vhd->jwt_audience; + ck.cookie_name = "sai_jwt"; + ck.extra_json = "\"authorized\": 1"; + ck.expiry_unix_time = 20 * 60; + + if (lws_jwt_sign_token_set_http_cookie(wsi, &ck, &p, end)) + goto clean_spa; + + /* + * Auth succeeded, go to the page the form was on + */ + + if (lws_add_http_header_by_token(wsi, + WSI_TOKEN_HTTP_LOCATION, + (unsigned char *)sr, + strlen((const char *)sr), + &p, end)) { + goto clean_spa; + } + + if (pss->spa) { + lws_spa_destroy(pss->spa); + pss->spa = NULL; + } + + if (lws_finalize_write_http_header(wsi, start, &p, end)) + goto bail; + + lwsl_notice("%s: setting cookie OK\n", __func__); + lwsl_hexdump_notice(start, lws_ptr_diff(p, start)); + return 0; + +final: + /* + * Auth failed, go back to / + */ + if (lws_add_http_header_by_token(wsi, + WSI_TOKEN_HTTP_LOCATION, + (unsigned char *)"/", 1, + &p, end)) { + goto clean_spa; + } + if (lws_finalize_write_http_header(wsi, start, &p, end)) + goto bail; + return 0; + } + if (pss->spa) { lws_spa_destroy(pss->spa); pss->spa = NULL; @@ -659,6 +934,14 @@ http_resp: return -1; break; +clean_spa: + if (pss->spa) { + lws_spa_destroy(pss->spa); + pss->spa = NULL; + } + pss->spa_failed = 1; + goto final; + /* * ws connections from builders and browsers */ @@ -693,17 +976,32 @@ http_resp: case LWS_CALLBACK_ESTABLISHED: - // lwsl_notice("%s: wsi %p: ESTABLISHED\n", __func__, wsi); -#if 0 - vhd->gsp->callback(wsi, LWS_CALLBACK_SESSION_INFO, - pss->pss_gs, &pss->sinfo, 0); - if (!pss->sinfo.username[0]) { - lwsl_notice("sai ws attempt with no session\n"); - - return -1; - } -#endif + /* + * What's the situation with a JWT cookie? Normal users won't + * have any, but privileged users will have one, and we should + * try to confirm it and set the pss auth level accordingly + */ + memset(&ck, 0, sizeof(ck)); + ck.jwk = &vhd->jwt_jwk_auth; + ck.alg = vhd->jwt_auth_alg; + ck.iss = vhd->jwt_issuer; + ck.aud = vhd->jwt_audience; + ck.cookie_name = "__Host-sai_jwt"; + + cml = sizeof(buf); + if (!lws_jwt_get_http_cookie_validate_jwt(wsi, &ck, + (char *)buf, &cml) && + ck.extra_json && + !lws_json_simple_strcmp(ck.extra_json, ck.extra_json_len, + "\"authorized\":", "1")) { + /* the token allows him to manage us */ + pss->authorized = 1; + pss->expiry_unix_time = ck.expiry_unix_time; + lws_strncpy(pss->auth_user, ck.sub, + sizeof(pss->auth_user)); + } else + lwsl_err("%s: cookie rejected\n", __func__); pss->wsi = wsi; pss->vhd = vhd; pss->alang[0] = '\0'; diff --git a/src/master/m-private.h b/src/master/m-private.h index de1883d..b2f15c4 100644 --- a/src/master/m-private.h +++ b/src/master/m-private.h @@ -30,6 +30,7 @@ typedef struct sai_platm { struct lws_dll2_owner subs_owner; sqlite3 *pdb; + sqlite3 *pdb_auth; } saim_t; typedef struct sai_platform { @@ -117,6 +118,7 @@ struct pss { char sub_task_uuid[65]; char specific[65]; char specific_project[96]; + char auth_user[33]; sqlite3 *pdb_artifact; sqlite3_blob *blob_artifact; @@ -146,6 +148,7 @@ struct pss { int log_cache_size; int authorized; int specificity; + unsigned long expiry_unix_time; /* notification hmac information */ char notification_sig[128]; @@ -153,6 +156,7 @@ struct pss { struct lws_genhmac_ctx hmac; enum lws_genhmac_types hmac_type; char our_form; + char login_form; uint64_t first_log_timestamp; uint64_t artifact_offset; @@ -192,6 +196,12 @@ struct vhd { struct lws_dll2_owner browsers; struct lws_dll2_owner builders; + /* our keys */ + struct lws_jwk jwt_jwk_auth; + char jwt_auth_alg[16]; + const char *jwt_issuer; + const char *jwt_audience; + const char *sqlite3_path_lhs; lws_dll2_owner_t sqlite3_cache; /* saim_sqlite_cache_t */ diff --git a/src/master/m-ws-browser.c b/src/master/m-ws-browser.c index cf16293..8883e27 100644 --- a/src/master/m-ws-browser.c +++ b/src/master/m-ws-browser.c @@ -101,6 +101,9 @@ enum { typedef struct sai_browse_taskreply { const sai_event_t *event; const sai_task_t *task; + char auth_user[33]; + int authorized; + int auth_secs; } sai_browse_taskreply_t; static lws_struct_map_t lsm_taskreply[] = { @@ -108,6 +111,9 @@ static lws_struct_map_t lsm_taskreply[] = { lsm_event, "e"), LSM_CHILD_PTR (sai_browse_taskreply_t, task, sai_task_t, NULL, lsm_task, "t"), + LSM_CARRAY (sai_browse_taskreply_t, auth_user, "auth_user"), + LSM_UNSIGNED (sai_browse_taskreply_t, authorized, "authorized"), + LSM_UNSIGNED (sai_browse_taskreply_t, auth_secs, "auth_secs"), }; const lws_struct_map_t lsm_schema_json_map_taskreply[] = { @@ -120,6 +126,19 @@ enum sai_overview_state { SOS_TASKS, }; +/* 1 == authorized */ + +static int +saim_conn_auth(struct pss *pss) +{ + if (!pss->authorized) + return 0; + if (pss->expiry_unix_time < (unsigned long)lws_now_secs()) + return 0; + + return 1; +} + /* * Ask for writeable cb on all browser connections subscribed to a particular * task (so we can send them some more logs) @@ -341,6 +360,9 @@ saim_ws_json_rx_browser(struct vhd *vhd, struct pss *pss, uint8_t *buf, case SAIM_WS_BROWSER_RX_TASKRESET: + if (!saim_conn_auth(pss)) + goto soft_error; + /* * User is asking us to reset / rebuild this task */ @@ -359,6 +381,9 @@ saim_ws_json_rx_browser(struct vhd *vhd, struct pss *pss, uint8_t *buf, case SAIM_WS_BROWSER_RX_EVENTRESET: + if (!saim_conn_auth(pss)) + goto soft_error; + /* * User is asking us to reset / rebuild every task in the event */ @@ -420,6 +445,9 @@ saim_ws_json_rx_browser(struct vhd *vhd, struct pss *pss, uint8_t *buf, * User is asking us to delete the whole event */ + if (!saim_conn_auth(pss)) + goto soft_error; + ei = (sai_browse_rx_evinfo_t *)a.dest; lwsl_notice("%s: received request to delete event %s\n", @@ -494,6 +522,10 @@ saim_ws_json_rx_browser(struct vhd *vhd, struct pss *pss, uint8_t *buf, goto bail;; case SAIM_WS_BROWSER_RX_TASKCANCEL: + + if (!saim_conn_auth(pss)) + goto soft_error; + /* * Browser is informing us of task's STOP button clicked, we * need to inform any builder that might be building it @@ -540,7 +572,7 @@ saim_ws_json_tx_browser(struct vhd *vhd, struct pss *pss, uint8_t *buf, size_t b sai_browse_taskreply_t task_reply; lws_dll2_owner_t task_owner; lws_struct_serialize_t *js; - char esc[256], filt[128]; + char esc[256], esc1[33], filt[128]; char event_uuid[33]; sqlite3 *pdb = NULL; sai_event_t *e; @@ -705,8 +737,21 @@ again: p += lws_snprintf((char *)p, end - p, "{\"schema\":\"sai.warmcat.com.overview\"," " \"alang\":\"%s\"," + " \"authorized\": %d," + " \"auth_secs\": %ld," + " \"auth_user\": \"%s\"," "\"overview\":[", - lws_json_purify(esc, pss->alang, sizeof(esc) - 1, &iu)); + lws_json_purify(esc, pss->alang, sizeof(esc) - 1, &iu), + pss->authorized, pss->expiry_unix_time - lws_now_secs(), + lws_json_purify(esc1, pss->auth_user, sizeof(esc1) - 1, &iu) + ); + + /* + * "authorized" here is used to decide whether to render the + * additional controls clientside. The events the controls + * cause if used are separately checked for coming from an + * authorized pss when they are received. + */ if (pss->specificity) pss->walk = lws_dll2_get_head(&pss->query_owner); @@ -880,8 +925,13 @@ so_finish: p += lws_snprintf((char *)p, end - p, "{\"schema\":\"com.warmcat.sai.builders\"," " \"alang\":\"%s\"," - "\"builders\":[", - lws_sql_purify(esc, pss->alang, sizeof(esc) - 1)); + " \"authorized\":%d," + " \"auth_secs\":%ld," + " \"auth_user\": \"%s\"," + " \"builders\":[", + lws_sql_purify(esc, pss->alang, sizeof(esc) - 1), + pss->authorized, pss->expiry_unix_time - lws_now_secs(), + lws_json_purify(esc1, pss->auth_user, sizeof(esc1) - 1, &iu)); pss->walk = lws_dll2_get_head(&vhd->master.builder_owner); pss->subsequent = 0; @@ -957,6 +1007,10 @@ b_finish: task_reply.event = pss->one_event; task_reply.task = pss->one_task; + task_reply.auth_secs = pss->expiry_unix_time - lws_now_secs(); + task_reply.authorized = pss->authorized; + lws_strncpy(task_reply.auth_user, pss->auth_user, + sizeof(task_reply.auth_user)); js = lws_struct_json_serialize_create(lsm_schema_json_map_taskreply, LWS_ARRAY_SIZE(lsm_schema_json_map_taskreply),
Page fetched 0s ago, creation time: 8ms (vhost etag hits: 0%, cache hits: 0%)