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

@@ -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.
+
+
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> ";
- 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) + "\"> ";
- 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) + "\"> " +
- 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) + "\"> ";
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),