Author: Andy Green Date: Sun Mar 10 06:07:40 2019 +0000 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b8ce6a0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.cproject +.project +build diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..b46a3bc --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,141 @@ +cmake_minimum_required(VERSION 2.8) +include(CheckCSourceCompiles) + +set(SRCS_M + src/master/sai-master.c + src/master/conf.c + src/master/ws.c +) +set(SRCS_B + src/builder/sai-builder.c + src/builder/conf.c + src/builder/protocol.c +) + +set(LIB_DIR lib CACHE PATH "Install dir for libraries") +set(BIN_DIR bin CACHE PATH "Install dir for executables") +set(INCLUDE_DIR include CACHE PATH "Install dir for header files") +set(DATA_DIR share CACHE PATH "Install dir for data files") + +# +# let the code know where the assets were installed +# +set(CMAKE_C_FLAGS "-D LWS_DATA_DIR=\\\"${CMAKE_INSTALL_PREFIX}/${DATA_DIR}\\\" ${CMAKE_C_FLAGS}") + + +# If we are being built as part of lws, confirm current build config supports +# reqconfig, else skip building ourselves. +# +# If we are being built externally, confirm installed lws was configured to +# support reqconfig, else error out with a helpful message about the problem. +# +MACRO(require_lws_config reqconfig _val result) + + if (DEFINED ${reqconfig}) + if (${reqconfig}) + set (rq 1) + else() + set (rq 0) + endif() + else() + set(rq 0) + endif() + + if (${_val} EQUAL ${rq}) + set(SAME 1) + else() + set(SAME 0) + endif() + + if (LWS_WITH_MINIMAL_EXAMPLES AND NOT ${SAME}) + if (${_val}) + message("${SAMP}: skipping as lws being built without ${reqconfig}") + else() + message("${SAMP}: skipping as lws built with ${reqconfig}") + endif() + set(${result} 0) + else() + if (LWS_WITH_MINIMAL_EXAMPLES) + set(MET ${SAME}) + else() + CHECK_C_SOURCE_COMPILES("#include \nint main(void) {\n#if defined(${reqconfig})\n return 0;\n#else\n fail;\n#endif\n return 0;\n}\n" HAS_${reqconfig}) + if (NOT DEFINED HAS_${reqconfig} OR NOT HAS_${reqconfig}) + set(HAS_${reqconfig} 0) + else() + set(HAS_${reqconfig} 1) + endif() + if ((HAS_${reqconfig} AND ${_val}) OR (NOT HAS_${reqconfig} AND NOT ${_val})) + set(MET 1) + else() + set(MET 0) + endif() + endif() + if (NOT MET) + if (${_val}) + message(FATAL_ERROR "This project requires lws must have been configured with ${reqconfig}") + else() + message(FATAL_ERROR "Lws configuration of ${reqconfig} is incompatible with this project") + endif() + endif() + + endif() +ENDMACRO() + +set(requirements 1) +require_lws_config(LWS_ROLE_H1 1 requirements) +require_lws_config(LWS_ROLE_WS 1 requirements) +require_lws_config(LWS_WITHOUT_SERVER 0 requirements) +require_lws_config(LWS_WITHOUT_CLIENT 0 requirements) +require_lws_config(LWS_WITH_UNIX_SOCK 1 requirements) + +if (requirements) + add_executable("sai-master" ${SRCS_M}) + add_executable("sai-builder" ${SRCS_B}) + + if (CMAKE_COMPILER_IS_GNUCC OR CMAKE_COMPILER_IS_GNUCXX OR (CMAKE_C_COMPILER_ID MATCHES "Clang") OR (CMAKE_CXX_COMPILER_ID MATCHES "Clang")) + set(CMAKE_C_FLAGS "-Wall -Wsign-compare -Wignored-qualifiers -Wtype-limits -Wuninitialized -Werror -Wundef ${CMAKE_C_FLAGS}" ) + endif() + + if (websockets_shared) + target_link_libraries("sai-master" websockets_shared) + add_dependencies("sai-master" websockets_shared) + + target_link_libraries("sai-builder" websockets_shared) + add_dependencies("sai-builder" websockets_shared) + else() + target_link_libraries("sai-master" websockets) + target_link_libraries("sai-builder" websockets) + endif() + +# +# libgit2 paths +# +find_path( GIT2_INC_PATH NAMES "git2.h") +find_library(GIT2_LIB_PATH NAMES "git2") + +if (GIT2_INC_PATH AND GIT2_LIB_PATH) + include_directories(BEFORE "${GIT2_INC_PATH}") +else() + message(FATAL_ERROR " Unable to find libgit2") +endif() + +target_link_libraries("sai-master" ${GIT2_LIB_PATH}) +target_link_libraries("sai-builder" ${GIT2_LIB_PATH}) + + install(TARGETS sai-master sai-builder + + LIBRARY DESTINATION "${LIB_DIR}${LIB_SUFFIX}" COMPONENT libraries + ARCHIVE DESTINATION "${LIB_DIR}${LIB_SUFFIX}" COMPONENT libraries + RUNTIME DESTINATION "${BIN_DIR}" COMPONENT libraries + PUBLIC_HEADER DESTINATION "${INCLUDE_DIR}" COMPONENT dev) + + install(FILES assets/index.html + assets/sai.css + assets/sai.js + assets/lws-common.js + assets/sai.svg + assets/favicon.ico + assets/strict-csp.svg + DESTINATION "${DATA_DIR}/sai/assets") + +endif() diff --git a/README.md b/README.md new file mode 100644 index 0000000..8a9b677 --- /dev/null +++ b/README.md @@ -0,0 +1,61 @@ +![Sai](./assets/sai.svg) + +`Sai` (pronouced like 'sigh', "Trial" in Japanese) is a very lightweight +network-aware remote CI runner. It knows how to use `systemd-nspawn` and +`overlayfs` to build remote git repos on a variety of distro versions very +cheaply. + +On small targets, it can also build on the host rootfs directly, and can also +run on an RPi3-class 'buddy' to flash and collect results (eg, over serial, or +gpio) from even smaller targets. + +A self-assembling constellation of Sai Builders make their own connections to a +master, who then received hook notifications, distributes work concurrently over +idle builders that have the required environment, and logs results. + +## General approach + + - Sai builders for various platforms and scenarios are configured and run + independently... they maintain "nailed-up" connections to the master over + wss, and announce their capabilities. The builders do not need any listening + ports or other central management this way. + + - When a git repo being monitored is updated, a hook performs a GET notiftying + the Sai master. + + - The Sai master fetches `.sai.json` from the repo and queues jobs on connected + builders that have the platform and OS versions requested for build in + the `.sai.json`. + + - On the builders, result channels (like stdout, stderr) with logs and + results are first listed and then streamed back to the master and the build + proceeds. Discrete files may also be streamed to the master like this. + + - The master makes human readable current and historical results available + in realtime over https + + - The Sai master node also has a builder integrated. So to get started you + can do it all on one machine. + +## Creating a selfsigned TLS cert + +You can create a P-521 EC key and self-signed certificate for use with Sai +master like this: + +``` +$ openssl ecparam -name secp521r1 -genkey -param_enc explicit -out sai-selfsigned-key.pem +$ echo -e "\n\n\n\n\n\n" | openssl req -new -x509 -key sai-selfsigned-key.pem -out sai-selfsigned-cert.pem +``` + +There's a helper script `sudo ./scripts/gen-selfsigned-certs.sh` to do this and +install the results in `/usr/local/share/sai`, you can override the install +directory by giving it as a commandline argument to the script. + +Or alternatively you can use your own PEM certificate and key from a trusted +certifcate vendor, which matches your domain name. + +Self-signed is secure, but your browser will make you explicitly trust it before +it will let you use it. Afterwards, your browser will notice if the certificate +is different and untrusted for that site and warn you. Because you must +generate it after install, your selfsigned cert will be unique. + diff --git a/assets/404.html b/assets/404.html new file mode 100644 index 0000000..3e5a14b --- /dev/null +++ b/assets/404.html @@ -0,0 +1,9 @@ + + + +
+

404

+ Sorry, that file doesn't exist. + + + diff --git a/assets/favicon.ico b/assets/favicon.ico new file mode 100644 index 0000000..c0cc2e3 Binary files /dev/null and b/assets/favicon.ico differ diff --git a/assets/index.html b/assets/index.html new file mode 100644 index 0000000..11a3f7b --- /dev/null +++ b/assets/index.html @@ -0,0 +1,17 @@ + + + + + + + + + +
+ +
+ + + diff --git a/assets/lws-common.js b/assets/lws-common.js new file mode 100644 index 0000000..5d56ca2 --- /dev/null +++ b/assets/lws-common.js @@ -0,0 +1,128 @@ +/* + * This section around grayOut came from here: + * http://www.codingforums.com/archive/index.php/t-151720.html + * Assumed public domain + * + * Init like this in your main html script, this also reapplies the gray + * + * lws_gray_out(true,{'zindex':'499'}); + * + * To remove the gray + * + * lws_gray_out(false); + * + */ + +function gsize(ptype) +{ + var h = document.compatMode === "CSS1Compat" && + !window.opera ? + document.documentElement.clientHeight : + document.body.clientHeight; + var w = document.compatMode === "CSS1Compat" && + !window.opera ? + document.documentElement.clientWidth : + document.body.clientWidth; + var pageWidth, pageHeight, t; + + if (document.body && + (document.body.scrollWidth || document.body.scrollHeight)) { + t = document.body.scrollWidth; + pageWidth = (w > t) ? ("" + w + "px") : ("" + (t) + "px"); + t = document.body.scrollHeight; + pageHeight = (h > t) ? ("" + h + "px") : ("" + (t) + "px"); + } else if (document.body.offsetWidth) { + t = document.body.offsetWidth; + pageWidth = (w > t) ? ("" + w + "px") : ("" + (t) + "px"); + t = document.body.offsetHeight; + pageHeight =(h > t) ? ("" + h + "px") : ("" + (t) + "px"); + } else { + pageWidth = "100%"; + pageHeight = "100%"; + } + return (ptype === 1) ? pageWidth : pageHeight; +} + +function addEvent( obj, type, fn ) { + if ( obj.attachEvent ) { + obj["e" + type + fn] = fn; + obj[type+fn] = function() { obj["e" + type + fn]( window.event );}; + obj.attachEvent("on" + type, obj[type + fn]); + } else + obj.addEventListener(type, fn, false); +} + +function removeEvent( obj, type, fn ) { + if ( obj.detachEvent ) { + obj.detachEvent("on" + type, obj[type + fn]); + obj[type + fn] = null; + } else + obj.removeEventListener(type, fn, false); +} + +function lws_gray_out(vis, _options) { + + var options = _options || {}; + var zindex = options.zindex || 50; + var opacity = options.opacity || 70; + var opaque = (opacity / 100); + var bgcolor = options.bgcolor || "#000000"; + var dark = document.getElementById("darkenScreenObject"); + + if (!dark) { + var tbody = document.getElementsByTagName("body")[0]; + var tnode = document.createElement("div"); + tnode.style.position = "absolute"; + tnode.style.top = "0px"; + tnode.style.left = "0px"; + tnode.style.overflow = "hidden"; + tnode.style.display ="none"; + tnode.id = "darkenScreenObject"; + tbody.appendChild(tnode); + dark = document.getElementById("darkenScreenObject"); + } + if (vis) { + dark.style.opacity = opaque; + dark.style.MozOpacity = opaque; + // dark.style.filter ='alpha(opacity='+opacity+')'; + dark.style.zIndex = zindex; + dark.style.backgroundColor = bgcolor; + dark.style.width = gsize(1); + dark.style.height = gsize(0); + dark.style.display = "block"; + addEvent(window, "resize", + function() { + dark.style.height = gsize(0); + dark.style.width = gsize(1); + } + ); + } else { + dark.style.display = "none"; + removeEvent(window, "resize", + function() { + dark.style.height = gsize(0); + dark.style.width = gsize(1); + } + ); + } +} + +/* + * end of grayOut related stuff + */ + +function new_ws(urlpath, protocol) +{ + if (typeof MozWebSocket != "undefined") + return new MozWebSocket(urlpath, protocol); + + return new WebSocket(urlpath, protocol); +} + +function lws_san(s) +{ + if (s.search("<") !== -1) + return "invalid string"; + + return s; +} diff --git a/assets/sai.css b/assets/sai.css new file mode 100644 index 0000000..c0be30b --- /dev/null +++ b/assets/sai.css @@ -0,0 +1,216 @@ + +span.title { + font-size:18pt; + font-family: Arial; + font-weight:normal; + text-align:center; + color:#000000; +} +span.mount { + font-size:10pt; + font-family: Arial; + font-weight:normal; + text-align:center; + color:#000000; +} +span.mountname { + font-size:14pt; + font-family: Arial; + font-weight:bold; + text-align:center; + color:#404010; +} +span.n { + font-size:12pt; + font-family: Arial; + font-weight:normal; + text-align:center; + color:#808020; +} +span.sn { + font-size:12pt; + font-family: Arial; + font-weight:bold; + text-align:center; + color:#808020; +} +span.v { + font-size:12pt; + font-family: Arial; + font-weight:bold; + text-align:center; + color:#202020; +} +span.m1 { + font-size:12pt; + font-family: Arial; + font-weight:bold; + text-align:center; + color:#202020; +} +span.m2 { + font-size:12pt; + font-family: Arial; + font-weight:normal; + text-align:center; + color:#202020; +} + +.browser { font-size:12pt; font-family: Arial; font-weight:normal; text-align:center; color:#ffff00; vertical-align:middle; text-align:center; background:#d0b070; padding:12px; -webkit-border-radius:10px; border-radius:10px;} +.group2 { vertical-align:middle; + text-align:center; + background:#f0f0e0; + padding:12px; + -webkit-border-radius:10px; + border-radius:10px; } +.explain { vertical-align:middle; + text-align:center; + background:#f0f0c0; padding:12px; + -webkit-border-radius:10px; + border-radius:10px; + color:#404000; + padding:3px; +} +td.wsstatus { vertical-align:middle; width:200px; height:50px; + text-align:center; + background:#f0f0c0; padding:6px; + -webkit-border-radius:8px; + border-radius:8px; + color:#404000; } +.tdform { vertical-align:middle; width:200px; height:50px; + text-align:center; + background:#f0f0d0; padding:6px; + -webkit-border-radius:8px; + margin:10px; + border-radius:8px; + border: 1px solid black; + border-collapse: collapse;font-size:18pt; font-family: Arial; font-weight:normal; text-align:center; color:#000000; + color:#404000; } + +td.l { vertical-align:middle; + text-align:center; + background:#d0d0b0; + padding:3px; + -webkit-border-radius:3px; + border-radius:3px; } + +td.bigger { font-size:120%; } + +div.bgw { background:white } +div.conninfo { + border: solid 2px #e0d040; + padding: 4px; + width: 500px; + height:350px; + overflow: auto; +} +span.f12 { font-size:12pt } + +.content { vertical-align:top; text-align:center; background:#fffff0; padding:12px; -webkit-border-radius:10px; border-radius:10px; } +.canvas { vertical-align:top; text-align:center; background:#efefd0; padding:12px; -webkit-border-radius:10px; border-radius:10px; } +.tabs { + position: relative; + min-height: 750px; /* This part sucks */ + clear: both; + margin: 25px 0; +} +.tab { + float: left; +} +.tab label { + background: #eee; + padding: 10px; + border: 1px solid #ccc; + margin-left: -1px; + position: relative; + left: 1px; +} +.tab [type=radio] { + display: none; +} +.content { + position: absolute; + top: 28px; + left: 0; + background: white; + right: 0; + bottom: 0; + padding: 20px; + border: 1px solid #ccc; +} +[type=radio]:checked ~ label { + background: white; + border-bottom: 1px solid white; + z-index: 2; +} +[type=radio]:checked ~ label ~ .content { + z-index: 1; +} + +td.wsstatus { vertical-align:middle; width:200px; height:50px; + text-align:center; + background:#f0f0c0; padding:6px; + -webkit-border-radius:8px; + border-radius:8px; + color:#404000; } +td.l { vertical-align:middle; + text-align:center; + background:#d0d0b0; + padding:3px; + -webkit-border-radius:3px; + border-radius:3px; } +td.dl { vertical-align:middle; + text-align:center; + background:#c0c0c0; + padding:3px; + -webkit-border-radius:3px; + border-radius:3px; } +td.c { vertical-align:middle; + text-align:center; + background:#c0c0a0; + padding:3px; + -webkit-border-radius:3px; + border-radius:3px; } +td.c0 { vertical-align:middle; + text-align:center; + background:#b0b090; + padding:3px; + -webkit-border-radius:3px; + border-radius:3px; } +td.dc0 { vertical-align:middle; + text-align:center; + background:#a0a0a0; + padding:3px; + -webkit-border-radius:3px; + border-radius:3px; } +td.c1 { vertical-align:middle; + text-align:center; + background:#c0c0c0; + padding:3px; + -webkit-border-radius:3px; + border-radius:3px; } +td.t { vertical-align:middle; + text-align:center; + background:#e0e0c0; + padding:3px; + -webkit-border-radius:3px; + border-radius:3px; +} + + +table.summary { + width: 100%; +} +td.logo { + margin: 16px; + margin-right: 32px; + padding: 16px; +} +td.summary { + background: #f8f8f8; + width: 100%; + margin: 16px; + padding: 16px; +} + + \ No newline at end of file diff --git a/assets/sai.js b/assets/sai.js new file mode 100644 index 0000000..59477d8 --- /dev/null +++ b/assets/sai.js @@ -0,0 +1,251 @@ +(function() { + +/* + * We display untrusted stuff in html context... reject anything + * that has HTML stuff in it + */ + +function san(s) +{ + if (s.search("<") !== -1) + return "invalid string"; + + return s; +} + +function humanize(s) +{ + var i = parseInt(s, 10); + + if (i >= (1024 * 1024 * 1024)) + return (i / (1024 * 1024 * 1024)).toFixed(3) + "Gi"; + + if (i >= (1024 * 1024)) + return (i / (1024 * 1024)).toFixed(3) + "Mi"; + + if (i > 1024) + return (i / 1024).toFixed(3) + "Ki"; + + return s; +} + + var pos = 0; + +function get_appropriate_ws_url() +{ + var pcol; + var u = document.URL; + + /* + * We open the websocket encrypted if this page came on an + * https:// url itself, otherwise unencrypted + */ + + if (u.substring(0, 5) === "https") { + pcol = "wss://"; + u = u.substr(8); + } else { + pcol = "ws://"; + if (u.substring(0, 4) === "http") + u = u.substr(7); + } + + u = u.split("/"); + + return pcol + u[0]; +} + + +var sai, jso, s; + +function ws_open_server_status() +{ + sai = new WebSocket(get_appropriate_ws_url() + "/browse", "com-warmcat-sai"); + + console.log(get_appropriate_ws_url() + "/browse"); + + try { + sai.onopen = function() { + // document.getElementById("title").innerHTML = "Server Status (Active)"; + lws_gray_out(false); + }; + + sai.onmessage =function got_packet(msg) { + var u, ci, n; + console.log(msg.data); + if (msg.data.length < 100) + return; + jso = JSON.parse(msg.data); + u = parseInt(san(jso.i.uptime), 10); + + if (parseInt(jso.i.contexts[0].deprecated, 10) === 0) + s = "
"; + else + s = ""; + + for (ci = 0; ci < jso.i.contexts.length; ci++) { + + if (parseInt(jso.i.contexts[ci].deprecated, 10) === 0) + s += ""; + + } // context + s = s + "
"; + s += + "Server" + + "Server Version: " + + san(jso.i.version) + "
" + + "Host Uptime: " + + ((u / (24 * 3600)) | 0) + "d " + + (((u % (24 * 3600)) / 3600) | 0) + "h " + + (((u % 3600) / 60) | 0) + "m"; + if (jso.i.l1) + s = s + ", Host Load: " + san(jso.i.l1) + " "; + if (jso.i.l2) + s = s + san(jso.i.l2) + " "; + if (jso.i.l3) + s = s + san(jso.i.l3); + if (jso.i.l1) + s =s + ""; + + if (jso.i.statm) { + var sm = jso.i.statm.split(" "); + s += ", Virt stack + heap Usage: " + + humanize(parseInt(sm[0], 10) * 4096) + "B"; + } + + for (n = 0; n < jso.files.length; n++) { + s += "
" + san(jso.files[n].path) + ":
" + san(jso.files[n].val); + } + s += "
" + + "Active Context"; + else + s += "
" + + "Deprecated Context " + ci + ""; + + u = parseInt(san(jso.i.contexts[ci].context_uptime), 10); + s += "Server Uptime: " + + ((u / (24 * 3600)) | 0) + "d " + + (((u % (24 * 3600)) / 3600) | 0) + "h " + + (((u % 3600) / 60) | 0) + "m"; + + s = s + + "
" + + "Listening wsi: " + san(jso.i.contexts[ci].listen_wsi) + ", " + + "Current wsi alive: " + (parseInt(san(jso.i.contexts[ci].wsi_alive), 10) - + parseInt(san(jso.i.contexts[ci].listen_wsi), 10)) + "
" + + "Total Rx: " + humanize(san(jso.i.contexts[ci].rx)) +"B, " + + "Total Tx: " + humanize(san(jso.i.contexts[ci].tx)) +"B
" + + + "CONNECTIONS: HTTP/1.x: " + san(jso.i.contexts[ci].h1_conn) +", " + + "Websocket: " + san(jso.i.contexts[ci].ws_upg) +", " + + "H2 upgrade: " + san(jso.i.contexts[ci].h2_upg) +", " + + "H2 ALPN: " + san(jso.i.contexts[ci].h2_alpn) +", " + + "Rejected: " + san(jso.i.contexts[ci].rejected) +"
" + + + "TRANSACTIONS: HTTP/1.x: " + san(jso.i.contexts[ci].h1_trans) + ", " + + "H2: " + san(jso.i.contexts[ci].h2_trans) +", " + + "Total H2 substreams: " + san(jso.i.contexts[ci].h2_subs) +"
" + + + "CGI: alive: " + san(jso.i.contexts[ci].cgi_alive) + ", " + + "spawned: " + san(jso.i.contexts[ci].cgi_spawned) + + ""; + + for (n = 0; n < jso.i.contexts[ci].pt.length; n++) { + + if (parseInt(jso.i.contexts[ci].deprecated, 10) === 0) + s += ""; + + } + for (n = 0; n < jso.i.contexts[ci].vhosts.length; n++) { + if (parseInt(jso.i.contexts[ci].deprecated, 10) === 0) + s += ""; + } + + s += "
  service thread " + (n + 1); + else + s += "
  service thread " + (n + 1); + s += "" + + "fds: " + san(jso.i.contexts[ci].pt[n].fds_count) + " / " + + san(jso.i.contexts[ci].pt_fd_max) + ", "; + s = s + "ah pool: " + san(jso.i.contexts[ci].pt[n].ah_pool_inuse) + " / " + + san(jso.i.contexts[ci].ah_pool_max) + ", " + + "ah waiting list: " + san(jso.i.contexts[ci].pt[n].ah_wait_list); + + s = s + "
  vhost " + (n + 1); + else + s += "
  vhost " + (n + 1); + s += ""; + if (jso.i.contexts[ci].vhosts[n].use_ssl === "1") + s = s + "https://"; + else + s = s + "http://"; + s = s + san(jso.i.contexts[ci].vhosts[n].name) + ":" + + san(jso.i.contexts[ci].vhosts[n].port) + ""; + if (jso.i.contexts[ci].vhosts[n].sts === "1") + s = s + " (STS)"; + s = s +"
" + + + "Total Rx: " + humanize(san(jso.i.contexts[ci].vhosts[n].rx)) +"B, " + + "Total Tx: " + humanize(san(jso.i.contexts[ci].vhosts[n].tx)) +"B
" + + + "CONNECTIONS: HTTP/1.x: " + san(jso.i.contexts[ci].vhosts[n].h1_conn) +", " + + "Websocket: " + san(jso.i.contexts[ci].vhosts[n].ws_upg) +", " + + "H2 upgrade: " + san(jso.i.contexts[ci].vhosts[n].h2_upg) +", " + + "H2 ALPN: " + san(jso.i.contexts[ci].vhosts[n].h2_alpn) +", " + + "Rejected: " + san(jso.i.contexts[ci].vhosts[n].rejected) +"
" + + + "TRANSACTIONS: HTTP/1.x: " + san(jso.i.contexts[ci].vhosts[n].h1_trans) + ", " + + "H2: " + san(jso.i.contexts[ci].vhosts[n].h2_trans) +", " + + "Total H2 substreams: " + san(jso.i.contexts[ci].vhosts[n].h2_subs) +"
"; + + if (jso.i.contexts[ci].vhosts[n].mounts) { + s = s + ""; + + var m; + for (m = 0; m < jso.i.contexts[ci].vhosts[n].mounts.length; m++) { + s = s + ""; + } + s = s + "
MountpointOriginCache Policy
"; + s = s + "" + san(jso.i.contexts[ci].vhosts[n].mounts[m].mountpoint) + + "" + + san(jso.i.contexts[ci].vhosts[n].mounts[m].origin) + + ""; + if (parseInt(san(jso.i.contexts[ci].vhosts[n].mounts[m].cache_max_age), 10)) + s = s + "max-age: " + + san(jso.i.contexts[ci].vhosts[n].mounts[m].cache_max_age) + + ", reuse: " + + san(jso.i.contexts[ci].vhosts[n].mounts[m].cache_reuse) + + ", reval: " + + san(jso.i.contexts[ci].vhosts[n].mounts[m].cache_revalidate) + + ", inter: " + + san(jso.i.contexts[ci].vhosts[n].mounts[m].cache_intermediaries); + s = s + "
"; + } + s = s + "
"; + + document.getElementById("conninfo").innerHTML = s; + }; + + sai.onclose = function(){ +// document.getElementById("title").innerHTML = "Server Status (Disconnected)"; + lws_gray_out(true,{"zindex":"499"}); + + myVar = setTimeout(ws_open_server_status, 3000); + }; + } catch(exception) { + alert("

Error" + exception); + } +} + +/* stuff that has to be delayed until all the page assets are loaded */ + +window.addEventListener("load", function() { + + lws_gray_out(true,{"zindex":"499"}); + + ws_open_server_status(); + +}, false); + +}()); + diff --git a/assets/sai.svg b/assets/sai.svg new file mode 100644 index 0000000..bf913b2 --- /dev/null +++ b/assets/sai.svg @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/strict-csp.svg b/assets/strict-csp.svg new file mode 100644 index 0000000..cd128f1 --- /dev/null +++ b/assets/strict-csp.svg @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/etc-sai-EXAMPLE/builder/conf b/etc-sai-EXAMPLE/builder/conf new file mode 100644 index 0000000..83360cd --- /dev/null +++ b/etc-sai-EXAMPLE/builder/conf @@ -0,0 +1,41 @@ +{ + # + # where to find the nspawn contexts (scanned at runtime) + # + "systemd-nspawn": { + "path": "/home/sai/sai-nspawn", + "instances": 3, + "timeout": 1800 + }, + + # + # physically connected targets, like microconstroller boards + # + "target": [ + { + "name": "optee-hikey-arm64", + "timeout": 3600, + "consoles": [ + { + "name": "dbg", + "path": "/dev/by-id/usb-FTDI_FT232R_USB_UART_A50285BI-if00-port0", + "baudrate": 115200, + "mode": "n81" + } + ] + } + ], + + # + # masters we will accept jobs from + # + "masters": [ + { +# "url": "wss://warmcat.com/builder", + "url": "wss://localhost:443/builder", + "selfsigned": 1, + "priority": 1 + } + ] +} + diff --git a/etc-sai-EXAMPLE/master/conf b/etc-sai-EXAMPLE/master/conf new file mode 100644 index 0000000..58d027a --- /dev/null +++ b/etc-sai-EXAMPLE/master/conf @@ -0,0 +1,14 @@ +# these are the sai server global settings +# +# stuff related to each vhosts should go in one +# file per vhost in ./conf.d/ + +{ + "global": { + "uid": "48", # after init, run as apache user + "gid": "48", # after init, run as apache group + "server-string": "sai", + "init-ssl": "yes" + } +} + diff --git a/etc-sai-EXAMPLE/master/conf.d/localhost b/etc-sai-EXAMPLE/master/conf.d/localhost new file mode 100644 index 0000000..7ccda44 --- /dev/null +++ b/etc-sai-EXAMPLE/master/conf.d/localhost @@ -0,0 +1,127 @@ +# you should create one file like this per vhost in /etc/sai/conf.d + +{ + "vhosts": [{ + # this should match the external hostname for the vhost, + # like xyz.com + "name": "localhost", + # multiple vhosts can occupy the same port (SNI is used + # to resolve) + "port": "443", + # required for avatar cache + "enable-client-ssl": "on", + # "ipv6": "on", + "interface": "lo", + +# "ciphers": "DEFAULT", +# "tls13-ciphers": "DEFAULT", + + # You should store your real certificate key somewhere safer, + # eg on Fedora /etc/pki/tls/private, readable only by root. But + # since the right place for this differs by distro and these + # particular certs are not valuable, we get them from + # /usr/local/share where they are installed. + + "host-ssl-key": "/usr/local/share/sai/sai-selfsigned-key.pem", + "host-ssl-cert": "/usr/local/share/sai/sai-selfsigned-cert.pem", + + "mounts": [ + { + # static assets like js, css, fonts + "mountpoint": "/sai", + "default": "index.html", + "origin": "file:///usr/local/share/sai/assets", + "cache-max-age": "7200", + "cache-reuse": "1", + "cache-revalidate": "0", + "cache-intermediaries": "0", + "extra-mimetypes": { + ".zip": "application/zip", + ".map": "application/json", + ".ttf": "application/x-font-ttf" + } + }, + { + # semi-static cached avatar icons + "mountpoint": "/sai/avatar", + "origin": "callback://avatar-proxy", + "cache-max-age": "2400", + "cache-reuse": "1", + "cache-revalidate": "0", + "cache-intermediaries": "0" + } + ], + + # + # these headers, which will be sent on every transaction, make + # recent browsers definitively ban any external script sources, + # no matter what might manage to get injected later in the + # page. It's a last line of defence against any successful XSS. + # + + "headers": [{ + "content-security-policy": "default-src 'none'; img-src 'self' data: https://travis-ci.org https://api.travis-ci.org https://ci.appveyor.com https://scan.coverity.com; script-src 'self'; font-src 'self'; style-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'none';", + "x-content-type-options": "nosniff", + "x-xss-protection": "1; mode=block", + "x-frame-options": "deny", + "referrer-policy": "no-referrer" + }], + + "ws-protocols": [ + + {"com-warmcat-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 + # your vhost. It's not served directly but read from + # the filesystem. Although it's cached in memory by + # gitohashi, it checks for changes and reloads if + # changed automatically. + # + # Putting logos etc as svg in css is highly recommended, + # like fonts these can be transferred once with a loose + # caching policy. So in practice they cost very little, + # and allow the browser to compose the page without + # delay. + # + "html-file": "/usr/local/share/gitohashi/templates/gitohashi-example.html", + # + # vpath required at start of links into this vhost's + # gitohashi content for example if an external http + # server is proxying us, and has been told to direct + # URLs starting "/git" to us, this should be set to + # "/git/" so URLs we generate referring to our own pages + # can work. + # + "vpath": "/sai/", + # + # url mountpoint for the avatar cache + # + "avatar-url": "/sai/avatar/", + # + # libjsgit2 JSON cache... this + # should not be directly served + # + "cache-base": "/var/cache/libjsongit2", + # + # restrict the JSON cache size + # to 2GB + # + "cache-size": "2000000000", + # + # optional flags, b0 = 1 = blog mode + # + "flags": 0 + #"blog-repo-name": "myrepo" + }, + "avatar-proxy": { + "remote-base": "https://www.gravatar.com/avatar/", + # + # this dir is served via avatar-proxy + # + "cache-dir": "/var/cache/libjsongit2" + }} + ] + } + ] +} diff --git a/etc-sai-EXAMPLE/master/conf.d/unixskt b/etc-sai-EXAMPLE/master/conf.d/unixskt new file mode 100644 index 0000000..6c5fddc --- /dev/null +++ b/etc-sai-EXAMPLE/master/conf.d/unixskt @@ -0,0 +1,112 @@ +# you should create one file like this per vhost in /etc/sai/conf.d + +{ + "vhosts": [{ + # this has no special meaning but needs to be unique + "name": "unixskt", + "unix-socket": 1, + # multiple vhosts can exist with different unix skt names + "interface": "/var/run/sai-unixskt", + # required for avatar cache + "enable-client-ssl": "on", + + "mounts": [ + { + # static assets like js, css, fonts + "mountpoint": "/sai", + "default": "index.html", + "origin": "file:///usr/local/share/sai/assets", + "cache-max-age": "7200", + "cache-reuse": "1", + "cache-revalidate": "0", + "cache-intermediaries": "0", + "extra-mimetypes": { + ".zip": "application/zip", + ".map": "application/json", + ".ttf": "application/x-font-ttf" + } + }, + { + # semi-static cached avatar icons + "mountpoint": "/sai/avatar", + "origin": "callback://avatar-proxy", + "cache-max-age": "2400", + "cache-reuse": "1", + "cache-revalidate": "0", + "cache-intermediaries": "0" + } + ], + + # + # these headers, which will be sent on every transaction, make + # recent browsers definitively ban any external script sources, + # no matter what might manage to get injected later in the + # page. It's a last line of defence against any successful XSS. + # + + "headers": [{ + "content-security-policy": "default-src 'none'; img-src 'self' data: https://travis-ci.org https://api.travis-ci.org https://ci.appveyor.com https://scan.coverity.com; script-src 'self'; font-src 'self'; style-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'none';", + "x-content-type-options": "nosniff", + "x-xss-protection": "1; mode=block", + "x-frame-options": "deny", + "referrer-policy": "no-referrer" + }], + + "ws-protocols": [ + + {"com-warmcat-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 + # your vhost. It's not served directly but read from + # the filesystem. Although it's cached in memory by + # gitohashi, it checks for changes and reloads if + # changed automatically. + # + # Putting logos etc as svg in css is highly recommended, + # like fonts these can be transferred once with a loose + # caching policy. So in practice they cost very little, + # and allow the browser to compose the page without + # delay. + # + "html-file": "/usr/local/share/gitohashi/templates/gitohashi-example.html", + # + # vpath required at start of links into this vhost's + # gitohashi content for example if an external http + # server is proxying us, and has been told to direct + # URLs starting "/git" to us, this should be set to + # "/git/" so URLs we generate referring to our own pages + # can work. + # + "vpath": "/sai/", + # + # url mountpoint for the avatar cache + # + "avatar-url": "/sai/avatar/", + # + # libjsgit2 JSON cache... this + # should not be directly served + # + "cache-base": "/var/cache/libjsongit2", + # + # restrict the JSON cache size + # to 2GB + # + "cache-size": "2000000000", + # + # optional flags, b0 = 1 = blog mode + # + "flags": 0 + #"blog-repo-name": "myrepo" + }, + "avatar-proxy": { + "remote-base": "https://www.gravatar.com/avatar/", + # + # this dir is served via avatar-proxy + # + "cache-dir": "/var/cache/libjsongit2" + }} + ] + } + ] +} diff --git a/sai.json b/sai.json new file mode 100644 index 0000000..7e4290e --- /dev/null +++ b/sai.json @@ -0,0 +1,13 @@ +{ + "tests": [ + { + "maker": "linux-f29-x86_64", + "configs": [ + { "CMAKE_OPTS": "" }, + { "CMAKE_OPTS": "-DLWS_WITH_HTTP2=1" }, + { "CMAKE_OPTS": "-DLWS_WITHOUT_SERVER=1" } + ] + } + ] +} + diff --git a/scripts/gen-selfsigned-certs.sh b/scripts/gen-selfsigned-certs.sh new file mode 100755 index 0000000..261826b --- /dev/null +++ b/scripts/gen-selfsigned-certs.sh @@ -0,0 +1,17 @@ +#!/bin/sh + +DEST=/usr/local/share/sai +if [ ! -z "$1" ] ; then + DEST=$1 +fi + +sudo openssl ecparam -name secp256r1 -genkey -out $DEST/sai-selfsigned-key.pem +echo -e "\n\n\n\n\n\n" | \ +sudo openssl req -new -x509 \ + -sha256 \ + -subj "/CN=localhost" \ + -days 10000 \ + -key $DEST/sai-selfsigned-key.pem \ + -out $DEST/sai-selfsigned-cert.pem +echo + diff --git a/src/builder/conf.c b/src/builder/conf.c new file mode 100644 index 0000000..deb8588 --- /dev/null +++ b/src/builder/conf.c @@ -0,0 +1,340 @@ +/* + * sai-builder conf.c + * + * Copyright (C) 2019 Andy Green + * + * 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 +#include +#include +#include + +#include "private.h" + +static const char * const paths[] = { + "systemd-nspawn.path", + "systemd-nspawn.instances", + "systemd-nspawn.timeout", + + "target[]", + "target[].name", + "target[].timeout", + "target[].consoles[]", + "target[].consoles[].name", + "target[].consoles[].path", + "target[].consoles[].baudrate", + "target[].consoles[].mode", + + "masters[]", + "masters[].url", + "masters[].interface", + "masters[].selfsigned", + "masters[].priority", /* array of builder names */ +}; + +enum enum_paths { + LEJPM_NSPAWN_PATH, + LEJPM_NSPAWN_INSTANCES, + LEJPM_NSPAWN_TIMEOUT, + + LEJPM_TARGET, + LEJPM_TARGET_NAME, + LEJPM_TARGET_TIMEOUT, + LEJPM_TARGET_CONSOLES, + LEJPM_TARGET_CONSOLES_NAME, + LEJPM_TARGET_CONSOLES_PATH, + LEJPM_TARGET_CONSOLES_BAUDRATE, + LEJPM_TARGET_CONSOLES_MODE, + + LEJPM_MASTERS, + LEJPM_MASTERS_URL, + LEJPM_MASTERS_INTERFACE, + LEJPM_MASTERS_SELFSIGNED, + LEJPM_MASTERS_PRIORITY, +}; + +struct jpargs { + struct sai_builder *builder; + + struct sai_nspawn *nspawn; + struct sai_master *master; + struct sai_target *target; + struct sai_console *console; +}; + +#define SAI_BUILDER_CONF_CHUNK 512 +#define SAI_BUILDER_PATH_MAX 128 + +static int +sai_builder_scan_overlays_cb(const char *dirpath, void *user, + struct lws_dir_entry *lde) +{ + struct sai_nspawn *nspawn = (struct sai_nspawn *)user; + size_t s = strlen(lde->name); + struct sai_overlay *overlay; + + if (lde->type != LDOT_DIR) + return 0; + + if (strncmp(lde->name, "env-", 4)) + return 0; + + overlay = lwsac_use(&nspawn->builder->conf_head, sizeof(*overlay), + SAI_BUILDER_CONF_CHUNK); + if (!overlay) + return 1; + + memset(overlay, 0, sizeof(*overlay)); + overlay->name = lwsac_use(&nspawn->builder->conf_head, s + 1, + SAI_BUILDER_CONF_CHUNK); + if (!overlay->name) + return 1; + memcpy(overlay->name, lde->name, s + 1); + + lws_dll_add_front(&overlay->overlay_list, &nspawn->overlay_head); + + return 0; +} + +static int +sai_builder_scan_nspawn_cb(const char *dirpath, void *user, + struct lws_dir_entry *lde) +{ + struct sai_builder *builder = (struct sai_builder *)user; + size_t s = strlen(lde->name); + struct sai_nspawn *nspawn; + char path[256]; + + if (lde->type != LDOT_DIR) + return 0; + + nspawn = lwsac_use(&builder->conf_head, sizeof(*nspawn), + SAI_BUILDER_CONF_CHUNK); + if (!nspawn) + return 1; + + memset(nspawn, 0, sizeof(*nspawn)); + nspawn->builder = builder; + nspawn->name = lwsac_use(&builder->conf_head, s + 1, + SAI_BUILDER_CONF_CHUNK); + if (!nspawn->name) { + free(nspawn); + return 1; + } + memcpy((void *)nspawn->name, lde->name, s + 1); + + lws_dll_add_front(&nspawn->nspawn_list, &builder->nspawn_head); + + lws_snprintf(path, sizeof(path) - 1, "%s/%s", dirpath, lde->name); + + if (lws_dir(path, nspawn, sai_builder_scan_overlays_cb)) { + lwsl_err("%s: unable to scan nspawn dir %s\n", __func__, + builder->nspawn_path); + return 1; + } + + return 0; +} + +static signed char +sai_builder_conf_cb(struct lejp_ctx *ctx, char reason) +{ + struct jpargs *a = (struct jpargs *)ctx->user; + const char **pp; + int n; + +#if 0 + lwsl_notice(" %d: %s (%d)\n", reason, ctx->path, ctx->path_match); + for (n = 0; n < ctx->wildcount; n++) + lwsl_notice(" %d\n", ctx->wild[n]); +#endif + + if (reason == LEJPCB_OBJECT_START) { + switch (ctx->path_match - 1) { + case LEJPM_TARGET: + a->target = lwsac_use_zeroed(&a->builder->conf_head, + sizeof(*a->target), + SAI_BUILDER_CONF_CHUNK); + if (!a->target) + return 1; + lws_dll_add_front(&a->target->target_list, + &a->builder->target_head); + return 0; + + case LEJPM_TARGET_CONSOLES: + a->console = lwsac_use_zeroed(&a->builder->conf_head, + sizeof(*a->console), + SAI_BUILDER_CONF_CHUNK); + if (!a->console) + return 1; + lws_dll_add_front(&a->console->console_list, + &a->target->console_head); + return 0; + + case LEJPM_MASTERS: + a->master = lwsac_use_zeroed(&a->builder->conf_head, + sizeof(*a->master), + SAI_BUILDER_CONF_CHUNK); + if (!a->master) + return 1; + lws_dll_add_front(&a->master->master_list, + &a->builder->master_head); + return 0; + + default: + return 0; + } + } + + /* we only match on the prepared path strings */ + if (!(reason & LEJP_FLAG_CB_IS_VALUE) || !ctx->path_match) + return 0; + + if (reason == LEJPCB_VAL_NUM_INT) { + n = atoi(ctx->buf); + switch (ctx->path_match - 1) { + case LEJPM_NSPAWN_INSTANCES: + a->builder->nspawn_instances = n; + break; + case LEJPM_NSPAWN_TIMEOUT: + a->builder->nspawn_timeout = n; + break; + case LEJPM_TARGET_TIMEOUT: + a->target->timeout = n; + break; + case LEJPM_TARGET_CONSOLES_BAUDRATE: + a->console->baudrate = n; + break; + case LEJPM_MASTERS_SELFSIGNED: + a->master->accept_selfsigned = n; + break; + case LEJPM_MASTERS_PRIORITY: + a->master->priority = n; + break; + default: + break; + } + + return 0; + } + + if (reason != LEJPCB_VAL_STR_END) + return 0; + + /* only the end part of the string, where we know the length */ + + switch (ctx->path_match - 1) { + case LEJPM_NSPAWN_PATH: + pp = &a->builder->nspawn_path; + break; + + case LEJPM_TARGET_NAME: + pp = &a->target->name; + break; + + case LEJPM_TARGET_CONSOLES_NAME: + pp = &a->console->name; + break; + + case LEJPM_TARGET_CONSOLES_PATH: + pp = &a->console->path; + break; + + case LEJPM_TARGET_CONSOLES_MODE: + pp = &a->console->mode; + break; + + case LEJPM_MASTERS_URL: + pp = &a->master->url; + break; + + case LEJPM_MASTERS_INTERFACE: + pp = &a->master->interface; + break; + + default: + return 0; + } + + *pp = lwsac_use(&a->builder->conf_head, ctx->npos + 1, + SAI_BUILDER_CONF_CHUNK); + if (!*pp) + return 1; + memcpy((char *)(*pp), ctx->buf, ctx->npos); + ((char *)(*pp))[ctx->npos] = '\0'; + + return 0; +} + +int +sai_builder_config(struct sai_builder *builder, const char *d) +{ + unsigned char buf[128]; + struct lejp_ctx ctx; + struct jpargs a; + int n, m, fd; + + memset(&a, 0, sizeof(a)); + a.builder = builder; + + lws_snprintf((char *)buf, sizeof(buf) - 1, "%s/conf", d); + + 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); + lejp_construct(&ctx, sai_builder_conf_cb, &a, paths, LWS_ARRAY_SIZE(paths)); + + do { + n = read(fd, buf, sizeof(buf)); + if (!n) + break; + + m = (int)(signed char)lejp_parse(&ctx, buf, n); + } while (m == LEJP_CONTINUE); + + close(fd); + n = ctx.line; + lejp_destruct(&ctx); + + if (m < 0) { + lwsl_err("%s/conf(%u): parsing error %d: %s\n", d, n, m, + lejp_error_to_string(m)); + return 2; + } + + /* scan the configured nspawn directory for what we support */ + + if (builder->nspawn_path && + lws_dir(builder->nspawn_path, builder, sai_builder_scan_nspawn_cb)) { + lwsl_err("%s: unable to scan nspawn dir %s\n", __func__, + builder->nspawn_path); + + return 1; + } + + return 0; +} + +void +sai_builder_config_destroy(struct sai_builder *builder) +{ + lwsac_free(&builder->conf_head); +} diff --git a/src/builder/private.h b/src/builder/private.h new file mode 100644 index 0000000..0bc1347 --- /dev/null +++ b/src/builder/private.h @@ -0,0 +1,99 @@ +/* + * Sai builder definitions src/builder/private.h + * + * Copyright (C) 2019 Andy Green + * + * 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 + */ + +struct sai_master; +struct sai_builder; + +enum sai_lease_states { + SOS_IDLE, + SOS_RESERVED, + SOS_CONFIRMED +}; + +struct sai_lease { + struct sai_master *lessor; + enum sai_lease_states state; +}; + +struct sai_console { + struct lws_dll console_list; + + const char *name; + const char *path; + const char *mode; + int baudrate; +}; + +struct sai_target { + struct lws_dll target_list; + struct lws_dll console_head; + + const char *name; + + struct sai_lease lease; + + int timeout; +}; + +struct sai_master { + struct lws_dll master_list; + + struct lws *cwsi; + + const char *url; + const char *interface; + int priority; + + unsigned int accept_selfsigned:1; +}; + +struct sai_overlay { + struct lws_dll overlay_list; + + char *name; +}; + +struct sai_nspawn { + struct lws_dll nspawn_list; + struct lws_dll overlay_head; + + struct sai_builder *builder; + + const char *name; + + struct sai_lease lease; +}; + +struct sai_builder { + struct lws_dll master_head; + struct lws_dll target_head; + struct lws_dll nspawn_head; + + struct lwsac *conf_head; + + const char *nspawn_path; + int nspawn_instances; + int nspawn_timeout; +}; + +extern const struct lws_protocols protocol_com_warmcat_sai; +extern int sai_builder_config(struct sai_builder *builder, const char *d); +extern void sai_builder_config_destroy(struct sai_builder *builder); diff --git a/src/builder/protocol.c b/src/builder/protocol.c new file mode 100644 index 0000000..736b35a --- /dev/null +++ b/src/builder/protocol.c @@ -0,0 +1,344 @@ +/* + * sai-builder com-warmcat-sai client protocol implementation + * + * Copyright (C) 2019 Andy Green + * + * 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 +#include +#include + +#include "private.h" + +typedef enum { + PHASE_IDLE, + + PFL_FIRST = 128, + + PHASE_START_ATTACH = PFL_FIRST | 1, + PHASE_SUMM_TARGETS = 2, + PHASE_SUMM_NSPAWNS = 3, + PHASE_SUMM_NSPAWNS_OVERLAYS = 4, + +} cursor_phase_t; + +struct pss { + struct sai_target *target; + struct sai_console *console; + + struct sai_nspawn *nspawn; + struct sai_overlay *overlay; + + cursor_phase_t phase; +}; + +struct vhd { + struct lws_context *context; + struct lws_vhost *vhost; + + struct sai_builder builder; + + int *interrupted; + const char **config_dir; +}; + +static int +generate(struct vhd *vhd, struct pss *pss, char *buf, int len) +{ + struct sai_builder *b = &vhd->builder; + char *p = buf, *end = buf + len - 10; + + while (pss->phase != PHASE_IDLE && lws_ptr_diff(end, p) > 100) { + + switch (pss->phase) { + case PHASE_IDLE: + break; + + case PHASE_START_ATTACH: + p += lws_snprintf(p, lws_ptr_diff(end, p), + "{\"schema\":\"com-warmcat-sai-ba\",\n" + " \"hostname\":\"%s\",\n" + " \"nspawn-timeout\":%d,\n" + " \"targets\": [", + lws_canonical_hostname(vhd->context), + b->nspawn_timeout); + pss->target = lws_container_of(b->target_head.next, + struct sai_target, + target_list); + pss->phase = PHASE_SUMM_TARGETS; + break; + + case PHASE_SUMM_TARGETS: + if (pss->target) { + p += lws_snprintf(p, lws_ptr_diff(end, p), + "{\"name\":\"%s\"}", + pss->target->name); + if (pss->target->target_list.next) + *p++ = ','; + *p++ = '\n'; + *p = '\0'; + + pss->target = lws_container_of( + pss->target->target_list.next, + struct sai_target, target_list); + } + if (!pss->target) { + p += lws_snprintf(p, lws_ptr_diff(end, p), + "],\n\"nspawns\": ["); + pss->nspawn = lws_container_of( + b->nspawn_head.next, + struct sai_nspawn, nspawn_list); + pss->phase = PHASE_SUMM_NSPAWNS; + } + break; + + case PHASE_SUMM_NSPAWNS: + if (pss->nspawn) { + *p++ = '{'; + if (pss->nspawn->name) + p += lws_snprintf(p, lws_ptr_diff(end, p), + "\"name\":\"%s\",\n", + pss->nspawn->name); + p += lws_snprintf(p, lws_ptr_diff(end, p), + " \"overlays\": ["); + + pss->overlay = lws_container_of( + pss->nspawn->overlay_head.next, + struct sai_overlay, overlay_list); + if (pss->overlay) { + pss->phase = PHASE_SUMM_NSPAWNS_OVERLAYS; + break; + } +next_nspawn: + *p++ = ']'; + *p++ = '}'; + if (pss->nspawn->nspawn_list.next) + *p++ = ','; + *p++ = '\n'; + *p = '\0'; + pss->nspawn = lws_container_of( + pss->nspawn->nspawn_list.next, + struct sai_nspawn, nspawn_list); + } + if (!pss->nspawn) { + p += lws_snprintf(p, lws_ptr_diff(end, p), "]\n"); + p += lws_snprintf(p, lws_ptr_diff(end, p), "}\n"); + pss->phase = PHASE_IDLE; + } + break; + + case PHASE_SUMM_NSPAWNS_OVERLAYS: + if (pss->overlay) { + p += lws_snprintf(p, lws_ptr_diff(end, p), + "{\"name\":\"%s\"}\n", + pss->overlay->name); + pss->overlay = lws_container_of( + pss->overlay->overlay_list.next, + struct sai_overlay, overlay_list); + if (pss->overlay) { + + } + } + if (!pss->overlay) { + pss->phase = PHASE_SUMM_NSPAWNS; + goto next_nspawn; + } + break; + + default: + break; + } + } + + return lws_ptr_diff(p, buf); +} + +static int +connect_client(struct vhd *vhd, struct sai_master *master) +{ + struct lws_client_connect_info i; + const char *prot; + char url[128], host[64]; + + memset(&i, 0, sizeof(i)); + + strncpy(url, master->url, sizeof(url) - 1); + url[sizeof(url) - 1] = '\0'; + + if (lws_parse_uri(url, &prot, &i.address, &i.port, &i.path)) { + lwsl_err("%s: unable to parse url '%s'\n", __func__, url); + + return 1; + } + + lws_snprintf(host, sizeof(host), "%s:%u", i.address, i.port); + + i.context = vhd->context; + i.host = host; + i.origin = host; + + if (!strcmp(prot, "https") || !strcmp(prot, "wss")) + i.ssl_connection = LCCSCF_USE_SSL; + if (master->accept_selfsigned) + i.ssl_connection |= LCCSCF_ALLOW_SELFSIGNED; + i.vhost = vhd->vhost; + i.iface = master->interface; + i.pwsi = &master->cwsi; + i.opaque_user_data = (void *)master; + + lwsl_user("connecting to %s://%s:%d/%s\n", prot, i.address, i.port, i.path); + + return !lws_client_connect_via_info(&i); +} + +static void +schedule_callback(struct lws *wsi, int reason, int secs) +{ + lws_timed_callback_vh_protocol(lws_get_vhost(wsi), + lws_get_protocol(wsi), reason, secs); +} + +static int +callback_com_warmcat_sai(struct lws *wsi, enum lws_callback_reasons reason, + void *user, void *in, size_t len) +{ + struct pss *pss = (struct pss *)user; + struct vhd *vhd = (struct vhd *) + lws_protocol_vh_priv_get(lws_get_vhost(wsi), + lws_get_protocol(wsi)); + struct sai_master *master = + (struct sai_master *)lws_get_opaque_user_data(wsi); + uint8_t buf[1024 + LWS_PRE], *start = buf + LWS_PRE, + *end = buf + sizeof(buf) -1; + int n, m, f; + + switch (reason) { + + case LWS_CALLBACK_PROTOCOL_INIT: + vhd = lws_protocol_vh_priv_zalloc(lws_get_vhost(wsi), + lws_get_protocol(wsi), + sizeof(struct vhd)); + if (!vhd) + return -1; + + vhd->context = lws_get_context(wsi); + vhd->vhost = lws_get_vhost(wsi); + + /* get the pointer to "interrupted" we were passed in pvo */ + vhd->interrupted = (int *)lws_pvo_search( + (const struct lws_protocol_vhost_options *)in, + "interrupted")->value; + vhd->config_dir = (const char **)lws_pvo_search( + (const struct lws_protocol_vhost_options *)in, + "configdir")->value; + + if (sai_builder_config(&vhd->builder, *vhd->config_dir)) { + lwsl_err("%s: config failed\n", __func__); + + return 1; + } + + schedule_callback(wsi, LWS_CALLBACK_USER, 1); + break; + + case LWS_CALLBACK_PROTOCOL_DESTROY: + sai_builder_config_destroy(&vhd->builder); + break; + + case LWS_CALLBACK_CLIENT_ESTABLISHED: + pss->phase = PHASE_START_ATTACH; + lws_callback_on_writable(wsi); + break; + + case LWS_CALLBACK_CLIENT_WRITEABLE: + lwsl_user("LWS_CALLBACK_CLIENT_WRITEABLE\n"); + + if (pss->phase == PHASE_IDLE) + break; + + f = pss->phase & PFL_FIRST; + n = generate(vhd, pss, (char *)start, lws_ptr_diff(end, start)); + + m = lws_write(wsi, start, n, + lws_write_ws_flags(LWS_WRITE_TEXT, f, + pss->phase == PHASE_IDLE)); + if (m < n) { + lwsl_err("ERROR %d writing to ws socket\n", m); + return -1; + } + + if (pss->phase != PHASE_IDLE) + lws_callback_on_writable(wsi); + break; + + case LWS_CALLBACK_CLIENT_RECEIVE: + + break; + + case LWS_CALLBACK_CLIENT_CONNECTION_ERROR: + lwsl_err("CLIENT_CONNECTION_ERROR: %s\n", + in ? (char *)in : "(null)"); + master->cwsi = NULL; + schedule_callback(wsi, LWS_CALLBACK_USER, 1); + break; + + case LWS_CALLBACK_CLIENT_CLOSED: + lwsl_user("LWS_CALLBACK_CLIENT_CLOSED\n"); + master->cwsi = NULL; + schedule_callback(wsi, LWS_CALLBACK_USER, 1); + break; + + /* rate-limited client connect retries */ + + case LWS_CALLBACK_USER: + /* + * let's retry any master connections that aren't either + * ongoing or happy + */ + + m = 0; + lws_start_foreach_dll(struct lws_dll *, mp, + vhd->builder.master_head.next) { + struct sai_master *ma = lws_container_of(mp, + struct sai_master, master_list); + + if (!ma->cwsi && connect_client(vhd, ma)) + m = 1; + } lws_end_foreach_dll(mp); + + /* + * if somebody's already unhappy, schedule a retry. Otherwise + * wait until a connection close or error to do it then. + */ + if (m) + schedule_callback(wsi, LWS_CALLBACK_USER, 1); + break; + + default: + break; + } + + return 0; +} + +const struct lws_protocols protocol_com_warmcat_sai = { + "com-warmcat-sai", + callback_com_warmcat_sai, + sizeof(struct pss), + 1024, 0, NULL, 0 +}; diff --git a/src/builder/sai-builder.c b/src/builder/sai-builder.c new file mode 100644 index 0000000..1760330 --- /dev/null +++ b/src/builder/sai-builder.c @@ -0,0 +1,114 @@ +/* + * sai-builder + * + * Copyright (C) 2019 Andy Green + * + * 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 +#include +#include + +#include "private.h" + +static struct lws_context *context; +static const char *config_dir = "/etc/sai/builder"; +static int interrupted; + +static const struct lws_protocols *pprotocols[] = { + &protocol_com_warmcat_sai, + NULL +}; + +static const struct lws_protocol_vhost_options pvo_configdir = { + NULL, + NULL, + "configdir", /* pvo name */ + (void *)&config_dir /* pvo value */ +}; + +static const struct lws_protocol_vhost_options pvo_interrupted = { + &pvo_configdir, + NULL, + "interrupted", /* pvo name */ + (void *)&interrupted /* pvo value */ +}; + +static const struct lws_protocol_vhost_options pvo = { + NULL, /* "next" pvo linked-list */ + &pvo_interrupted, /* "child" pvo linked-list */ + "com-warmcat-sai", /* protocol name we belong to on this vhost */ + "" /* ignored */ +}; +static const struct lws_extension extensions[] = { + { + "permessage-deflate", + lws_extension_callback_pm_deflate, + "permessage-deflate" + "; client_no_context_takeover" + "; client_max_window_bits" + }, + { NULL, NULL, NULL /* terminator */ } +}; + +void sigint_handler(int sig) +{ + interrupted = 1; +} + +int main(int argc, const char **argv) +{ + struct lws_context_creation_info info; + const char *p; + int logs = LLL_USER | LLL_ERR | LLL_WARN | LLL_NOTICE; + + if ((p = lws_cmdline_option(argc, argv, "-d"))) + logs = atoi(p); + + lws_set_log_level(logs, NULL); + lwsl_user("Sai Builder - " + "Copyright (C) 2019 Andy Green \n"); + lwsl_user(" sai-builder [-c ]\n"); + + memset(&info, 0, sizeof info); /* otherwise uninitialized garbage */ + info.port = CONTEXT_PORT_NO_LISTEN; + info.pprotocols = pprotocols; + info.pvo = &pvo; + if (!lws_cmdline_option(argc, argv, "-n")) + info.extensions = extensions; + info.pt_serv_buf_size = 32 * 1024; + info.options = LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT | + LWS_SERVER_OPTION_VALIDATE_UTF8; + + if (lws_cmdline_option(argc, argv, "--libuv")) + info.options |= LWS_SERVER_OPTION_LIBUV; + else + signal(SIGINT, sigint_handler); + + context = lws_create_context(&info); + if (!context) { + lwsl_err("lws init failed\n"); + return 1; + } + + while (!lws_service(context, 1000) && !interrupted) + ; + + lws_context_destroy(context); + + return 0; +} diff --git a/src/master/conf.c b/src/master/conf.c new file mode 100644 index 0000000..6aacd4d --- /dev/null +++ b/src/master/conf.c @@ -0,0 +1,82 @@ +/* + * Sai master src/master/conf.c + * + * Copyright (C) 2019 Andy Green + * + * 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 +#include +#include +#include + +#define SAI_CONFIG_STRING_SIZE (16 * 1024) + +struct lws_context * +sai_lws_context_from_json(const char *config_dir, + const struct lws_protocols **pprotocols) +{ + int cs_len = SAI_CONFIG_STRING_SIZE - 1; + struct lws_context_creation_info info; + struct lws_context *context; + char *cs, *config_strings; + + cs = config_strings = malloc(SAI_CONFIG_STRING_SIZE); + if (!config_strings) { + lwsl_err("Unable to allocate config strings heap\n"); + + return NULL; + } + + memset(&info, 0, sizeof(info)); + + info.external_baggage_free_on_destroy = config_strings; + info.pt_serv_buf_size = 8192; + info.options = LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT | + LWS_SERVER_OPTION_EXPLICIT_VHOSTS | + LWS_SERVER_OPTION_HTTP_HEADERS_SECURITY_BEST_PRACTICES_ENFORCE | + LWS_SERVER_OPTION_VALIDATE_UTF8; + + lwsl_notice("Using config dir: \"%s\"\n", config_dir); + + /* + * first go through the config for creating the outer context + */ + if (lwsws_get_config_globals(&info, config_dir, &cs, &cs_len)) + goto init_failed; + + context = lws_create_context(&info); + if (context == NULL) { + lwsl_err("lws init failed\n"); + /* config_strings freed as 'external baggage' */ + return NULL; + } + + info.pprotocols = pprotocols; + + if (lwsws_get_config_vhosts(context, &info, config_dir, &cs, &cs_len)) { + lws_context_destroy(context); + + return NULL; + } + + return context; + +init_failed: + free(config_strings); + + return NULL; +} diff --git a/src/master/private.h b/src/master/private.h new file mode 100644 index 0000000..7ee6749 --- /dev/null +++ b/src/master/private.h @@ -0,0 +1,98 @@ +/* + * Sai master definitions src/master/private.h + * + * Copyright (C) 2019 Andy Green + * + * 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 + */ + +struct sai_master; + +enum sai_lease_states { + SOS_IDLE, + SOS_RESERVED, + SOS_CONFIRMED, + SOS_OCCUPIED, + SOS_GOING_DOWN +}; + +typedef struct sai_console { + struct lws_dll console_list; + + const char *name; + const char *path; + const char *mode; + int baudrate; +} sai_console_t; + +typedef struct sai_target { + struct lws_dll target_list; + struct lws_dll console_head; + + const char *name; + + enum sai_lease_states state; +} sai_target_t; + +typedef struct sai_nspawn { + struct lws_dll nspawn_list; + + const char *name; + + enum sai_lease_states state; +} sai_nspawn_t; + +typedef struct sai_builder { + struct lws_dll builder_list; + struct lws_dll target_head; + struct lws_dll nspawn_head; + + struct lws *wsi; + + char *hostname; +} sai_builder_t; + +typedef enum { + SJS_CLONING, + SJS_ASSIGNING, + SJS_WAITING, + SJS_DONE +} sai_job_state_t; + +typedef struct sai_job { + struct lws_dll jobs_list; + char reponame[64]; + char ref[64]; + char head[64]; + + time_t requested; + + sai_job_state_t state; + +} sai_job_t; + +typedef struct sai_master { + struct lws_dll builder_head; + + struct lws_dll pending_jobs_head; + struct lws_dll ongoing_jobs_head; +} sai_master_t; + + +extern struct lws_context * +sai_lws_context_from_json(const char *config_dir, + const struct lws_protocols **pprotocols); +extern const struct lws_protocols protocol_ws; diff --git a/src/master/sai-master.c b/src/master/sai-master.c new file mode 100644 index 0000000..138703c --- /dev/null +++ b/src/master/sai-master.c @@ -0,0 +1,68 @@ +/* + * Sai master + * + * Copyright (C) 2019 Andy Green + * + * 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 +#include +#include +#include + +#include "private.h" + +static int interrupted; + +static const struct lws_protocols + *pprotocols[] = { &protocol_ws, NULL }; + +static void sigint_handler(int sig) +{ + interrupted = 1; +} + +int main(int argc, const char **argv) +{ + int n = 0, logs = LLL_USER | LLL_ERR | LLL_WARN | LLL_NOTICE; + struct lws_context *context; + const char *p, *conf = "/etc/sai/master"; + + signal(SIGINT, sigint_handler); + + if ((p = lws_cmdline_option(argc, argv, "-d"))) + logs = atoi(p); + + lws_set_log_level(logs, NULL); + lwsl_user("Sai Master - Copyright (C) 2019 Andy Green \n"); + + if ((p = lws_cmdline_option(argc, argv, "-c"))) + conf = p; + + context = sai_lws_context_from_json(conf, pprotocols); + if (!context) { + lwsl_err("lws init failed\n"); + return 1; + } + + while (n >= 0 && !interrupted) + n = lws_service(context, 1000); + + lws_context_destroy(context); + + return 0; +} diff --git a/src/master/ws.c b/src/master/ws.c new file mode 100644 index 0000000..19aec34 --- /dev/null +++ b/src/master/ws.c @@ -0,0 +1,317 @@ +/* + * Sai master + * + * Copyright (C) 2019 Andy Green + * + * 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 + * + * The same ws interface is connected-to by builders (on path /builder), and + * provides the query transport for browsers (on path /browse). + */ + +#include +#include +#include +#include + +#include + +#include "private.h" + +typedef enum { + SWT_BUILDER, + SWT_BROWSE +} ws_type; + +struct pss { + ws_type type; +}; + +struct vhd { + struct lws_context *context; + struct lws_vhost *vhost; + + struct sai_master master; +}; + +/* + * {"schema":"com-warmcat-sai-builder-attach", + "hostname":"learn", "targets": [{"name":"optee-hikey-arm64"} +], +{"nspawns": []} + */ + + +static const char * const paths[] = { + "schema", + "hostname", + "nspawn-timeout", + + "targets[]", + "targets[].name", + + "nspawns[]", + "nspawns[].name", + "nspawns[].overlays[]", + "nspawns[].overlays[].name", +}; + +enum enum_paths { + SBAJP_SCHEMA, + SBAJP_HOSTNAME, + SBAJP_NSPAWN_TIMEOUT, + + SBAJP_TARGETS, + SBAJP_TARGETS_NAME, + + SBAJP_NSPAWNS, + SBAJP_NSPAWNS_NAME, + SBAJP_NSPAWNS_OVERLAYS, + SBAJP_NSPAWNS_OVERLAYS_NAME, +}; + +static int +sai_master_nspawn_destroy(struct lws_dll *d) +{ + sai_nspawn_t *n = lws_container_of(d, sai_nspawn_t, nspawn_list); + + lws_dll_remove(&n->nspawn_list); + free(n); + + return 0; +} + +static int +sai_master_console_destroy(struct lws_dll *d) +{ + sai_console_t *c = lws_container_of(d, sai_console_t, console_list); + + lws_dll_remove(&c->console_list); + free(c); + + return 0; +} + +static int +sai_master_target_destroy(struct lws_dll *d) +{ + sai_target_t *t = lws_container_of(d, sai_target_t, target_list); + + lws_dll_foreach_safe(&t->console_head, sai_master_console_destroy); + + free(t); + + return 0; +} + +static int +sai_master_builder_destroy(struct lws_dll *d) +{ + sai_builder_t *b = lws_container_of(d, sai_builder_t, builder_list); + + lws_dll_foreach_safe(&b->nspawn_head, sai_master_nspawn_destroy); + lws_dll_foreach_safe(&b->target_head, sai_master_target_destroy); + + if (b->hostname) + free(b->hostname); + + free(b); + + return 0; +} + +static int +sai_destroy_job(struct lws_dll *d) +{ + sai_job_t *job = lws_container_of(d, sai_job_t, jobs_list); + + lws_dll_remove(d); + free(job); + + return 0; +} + +static void +sai_master_master_destroy(struct sai_master *master) +{ + lws_dll_foreach_safe(&master->builder_head, sai_master_builder_destroy); + + lws_dll_foreach_safe(&master->pending_jobs_head, sai_destroy_job); + lws_dll_foreach_safe(&master->ongoing_jobs_head, sai_destroy_job); +} + +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 + 2048], *start = &buf[LWS_PRE], *p = start, + *end = &buf[sizeof(buf) - LWS_PRE - 1]; + struct pss *pss = (struct pss *)user; + struct sai_job *job; + const char *ccp; + int n; + + (void)end; + (void)p; + (void)paths; + + switch (reason) { + case LWS_CALLBACK_PROTOCOL_INIT: + vhd = lws_protocol_vh_priv_zalloc(lws_get_vhost(wsi), + lws_get_protocol(wsi), + sizeof(struct vhd)); + if (!vhd) + return -1; + + vhd->context = lws_get_context(wsi); + vhd->vhost = lws_get_vhost(wsi); + break; + + case LWS_CALLBACK_PROTOCOL_DESTROY: + sai_master_master_destroy(&vhd->master); + break; + + case LWS_CALLBACK_HTTP: + + n = HTTP_STATUS_FORBIDDEN; + + if (strcmp((const char *)in, "/sai/update-hook")) { + lwsl_notice("%s: unknown path %s\n", __func__, + (const char *)in); + goto http_resp; + } + + job = malloc(sizeof(*job)); + if (!job) { + lwsl_err("%s: OOM\n", __func__); + + return 1; + } + + ccp = lws_get_urlarg_by_name(wsi, "rn=", (char *)start, + sizeof(job->reponame)); + if (!ccp || !*ccp) { + lwsl_notice("%s: missing rn=\n", __func__); + free(job); + goto http_resp; + } + lws_strncpy(job->reponame, ccp, sizeof(job->reponame)); + ccp = lws_get_urlarg_by_name(wsi, "ref=", (char *)start + 128, + sizeof(job->ref)); + if (!ccp || !*ccp) { + lwsl_notice("%s: missing ref=\n", __func__); + free(job); + goto http_resp; + } + lws_strncpy(job->ref, ccp, sizeof(job->ref)); + ccp = lws_get_urlarg_by_name(wsi, "head=", (char *)start + 256, + sizeof(job->head)); + if (!ccp || !*ccp) { + lwsl_notice("%s: missing ref=\n", __func__); + free(job); + goto http_resp; + } + lws_strncpy(job->head, ccp, sizeof(job->head)); + + job->requested = time(NULL); + + ccp = strrchr(job->ref + strlen(job->ref) - 1, '/'); + if (ccp) + ccp++; + + lwsl_user("%s: added job: %s %s %s\n", __func__, job->reponame, + ccp, job->head); + + { + git_clone_options opts; + git_repository *repo; + const char *url = "https://libwebsockets.org/repo/libwebsockets"; + + git_clone_init_options(&opts, GIT_CLONE_OPTIONS_VERSION); + opts.checkout_branch = ccp; + + if (git_clone(&repo, url, "/tmp", &opts)) { + lwsl_err("%s: unable to clone\n", __func__); + free(job); + goto http_resp; + } + + lwsl_user("%s: clone OK\n", __func__); + } + + lws_dll_add_front(&job->jobs_list, &vhd->master.pending_jobs_head); + n = HTTP_STATUS_OK; + + /* faillthru */ + +http_resp: + if (lws_add_http_header_status(wsi, n, &p, end)) + goto bail; + if (lws_add_http_header_content_length(wsi, 0, &p, end)) + goto bail; + if (lws_finalize_write_http_header(wsi, start, &p, end)) + goto bail; + goto try_to_reuse; + + case LWS_CALLBACK_ESTABLISHED: + + if (lws_hdr_total_length(wsi, WSI_TOKEN_GET_URI)) { + if (lws_hdr_copy(wsi, (char *)start, 64, WSI_TOKEN_GET_URI) < 0) + return -1; + } else + if (lws_hdr_copy(wsi, (char *)start, 64, WSI_TOKEN_HTTP_COLON_PATH) < 0) + return -1; + + if (!strcmp((char *)start, "/browse")) { + n = SWT_BUILDER; + } else { + if (!strcmp((char *)start, "builder")) { + n = SWT_BROWSE; + } else { + lwsl_err("%s: unknown URL\n", __func__); + + return -1; + } + } + + pss->type = n; + break; + + case LWS_CALLBACK_RECEIVE: + lwsl_user("RECEIVE: %d\n", (int)len); + lwsl_user("%.*s\n", (int)len, (const char *)in); + break; + + default: + break; + } + + return lws_callback_http_dummy(wsi, reason, user, in, len); + +bail: + return 1; + +try_to_reuse: + if (lws_http_transaction_completed(wsi)) + return -1; + + return 0; +} + +const struct lws_protocols protocol_ws = + { "com-warmcat-sai", callback_ws, sizeof(struct pss), 0 };