🤞 Sync 2026-09-29 21:07:16

This commit is contained in:
github-actions[bot] committed 2026-09-29 21:07:16 +08:00
1 parent 52b5905a85
commit 08343f89fa
231 files changed
+179633 -1348

No files matched your search

+1
View File
@@ -0,0 +1 @@
c7fb12c3fac601feb975ed05c3fca594b0877e48
+16
View File
@@ -0,0 +1,16 @@
# upstream/ — 上游源码快照(vendor)
本目录是 **`meow-rs/meow-rs` 仓库 `openwrt/` 目录的原样快照**,由
`.github/workflows/sync-upstream.yml` 调用 `scripts/sync-upstream.sh` 自动刷新。
- `PIN` — 快照对应的上游 commit
- `OPENWRT_TREE` — 快照对应的上游 `openwrt/` tree SHA;同步脚本以此判断 `openwrt/` 是否真正发生变化
- `SNAPSHOT` — 上游仓库 / ref / commit / tree / 提交时间等同步元数据
- `openwrt/` — 上游 `openwrt/` 目录原样内容(含 `meow/files/`、`luci-app-meow/`、
`build-ipk.sh`、`docker/side-router.sh`)
## 约定
**不要手工修改本目录下的任何文件。** 上游一有 `openwrt/` 更新,这里会被整体刷新。
需要本地改动请写成 `patches/*.patch`,然后通过 `scripts/apply-patches.sh` 重新生成
`meow/` 与 `luci-app-meow/`。
+5
View File
@@ -0,0 +1,5 @@
UPSTREAM_REPO=meow-rs/meow-rs
UPSTREAM_REF=main
UPSTREAM_COMMIT=c7fb12c3fac601feb975ed05c3fca594b0877e48
UPSTREAM_COMMIT_DATE=2026-09-29T15:31:09+08:00
PREVIOUS_COMMIT=94f7acc9e9115254bd8937cff2025456cbda1b7c
+25
View File
@@ -0,0 +1,25 @@
# OpenWrt packaging
Packaging sources for the official OpenWrt `.ipk` release artifacts
(issue [#284](https://github.com/madeye/meow-rs/issues/284)).
- `build-ipk.sh` — assembles opkg-format `.ipk` packages from a prebuilt
static musl binary, without the OpenWrt SDK. Run with no arguments for
usage.
- `meow/files/` — procd init scripts (`meow.init`, `meow-arp.init`),
`/etc/config/meow` UCI settings, the default `/etc/meow/config.yaml`,
`gateway.sh` (transparent-proxy nftables rules for gateway / side-router
mode: kernel TPROXY or REDIRECT, plus the per-client MAC bypass set managed
from the LuCI Clients tab), and `arp-hijack.sh` (opt-in, off-by-default
ARP-based client steering, an advanced section of the Clients tab — ARP spoofing, for
devices you administer only).
- `luci-app-meow/` — LuCI app: `root/` overlays `/` on the device,
`htdocs/` maps to `/www`. Overview, config editor, clients, settings and log
views talk to the meow REST API and UCI; the Panel tab embeds the built-in
web UI served at `/ui` instead of reimplementing a dashboard.
- `docker/side-router.sh` — runs OpenWrt in a Docker container on the LAN
(macvlan, bridge mode) configured as a side router with meow installed.
Release wiring lives in `.github/workflows/release.yml` (ipk matrix), the
Docker end-to-end test in `tests/test_openwrt_docker.sh`, and user-facing
documentation in [docs/openwrt.md](../docs/openwrt.md).
+200
View File
@@ -0,0 +1,200 @@
#!/usr/bin/env bash
# Assemble OpenWrt .ipk packages (opkg format) without the OpenWrt SDK.
#
# The release binaries are fully static musl builds, so a single Rust target
# serves several OpenWrt architecture labels; the ipk only differs in the
# `Architecture:` control field. Works with both GNU tar and bsdtar (macOS).
#
# Usage:
# build-ipk.sh meow --binary <path> --version <ver> --arch <openwrt-arch> [--outdir <dir>]
# build-ipk.sh luci --version <ver> [--outdir <dir>]
#
# Examples:
# build-ipk.sh meow --binary target/aarch64-unknown-linux-musl/release/meow \
# --version 0.16.0 --arch aarch64_generic --outdir dist
# build-ipk.sh luci --version 0.16.0 --outdir dist
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
MAINTAINER="Max Lv <max.c.lv@gmail.com>"
usage() {
sed -n '2,15p' "$0" | sed 's/^# \{0,1\}//'
exit 1
}
# tar flags forcing root ownership: GNU tar and bsdtar spell them differently.
# --format=ustar is mandatory: bsdtar's default (restricted pax) emits
# extended-header entries that opkg's untar skips silently, producing an
# "installed" package with no files on the device.
if tar --version 2>/dev/null | grep -q "GNU tar"; then
TAR_OWNER=(--format=ustar --owner=0 --group=0 --numeric-owner)
else
TAR_OWNER=(--format=ustar --uid 0 --gid 0 --numeric-owner)
fi
# pack_ipk <staging-dir> <output-file>
# <staging-dir> must contain control/ (control, conffiles, postinst, ...)
# and data/ (the target filesystem tree).
pack_ipk() {
local staging="$1" out="$2"
printf '2.0\n' > "${staging}/debian-binary"
tar -C "${staging}/control" "${TAR_OWNER[@]}" -czf "${staging}/control.tar.gz" .
tar -C "${staging}/data" "${TAR_OWNER[@]}" -czf "${staging}/data.tar.gz" .
# opkg expects members in this exact order.
tar -C "${staging}" "${TAR_OWNER[@]}" -czf "${out}" \
./debian-binary ./control.tar.gz ./data.tar.gz
echo "built ${out}"
}
# installed_size <data-dir>
installed_size() {
find "$1" -type f -exec wc -c {} + | awk 'END { print $1 }'
}
build_meow() {
local binary="" version="" arch="" outdir="."
while [ $# -gt 0 ]; do
case "$1" in
--binary) binary="$2"; shift 2 ;;
--version) version="$2"; shift 2 ;;
--arch) arch="$2"; shift 2 ;;
--outdir) outdir="$2"; shift 2 ;;
*) echo "unknown option: $1" >&2; usage ;;
esac
done
[ -n "$binary" ] && [ -n "$version" ] && [ -n "$arch" ] || usage
[ -f "$binary" ] || { echo "binary not found: $binary" >&2; exit 1; }
local staging
staging="$(mktemp -d)"
mkdir -p "$staging/control" \
"$staging/data/usr/bin" \
"$staging/data/etc/init.d" \
"$staging/data/etc/config" \
"$staging/data/etc/meow" \
"$staging/data/etc/uci-defaults" \
"$staging/data/usr/share/meow"
install -m 755 "$binary" "$staging/data/usr/bin/meow"
install -m 755 "$SCRIPT_DIR/meow/files/meow.init" "$staging/data/etc/init.d/meow"
install -m 644 "$SCRIPT_DIR/meow/files/meow.config" "$staging/data/etc/config/meow"
install -m 644 "$SCRIPT_DIR/meow/files/config.yaml" "$staging/data/etc/meow/config.yaml"
install -m 755 "$SCRIPT_DIR/meow/files/gateway.sh" "$staging/data/usr/share/meow/gateway.sh"
install -m 755 "$SCRIPT_DIR/meow/files/arp-hijack.sh" "$staging/data/usr/share/meow/arp-hijack.sh"
install -m 755 "$SCRIPT_DIR/meow/files/meow-arp.init" "$staging/data/etc/init.d/meow-arp"
install -m 755 "$SCRIPT_DIR/meow/files/meow.uci-defaults" "$staging/data/etc/uci-defaults/80_meow"
cat > "$staging/control/control" <<EOF
Package: meow
Version: ${version}
Depends: libc
Section: net
Architecture: ${arch}
Installed-Size: $(installed_size "$staging/data")
Maintainer: ${MAINTAINER}
Description: A high-performance, rule-based tunneling proxy kernel in Rust,
compatible with mihomo (Clash Meta). Static binary; configuration lives in
/etc/meow/config.yaml, service settings in /etc/config/meow.
EOF
cat > "$staging/control/conffiles" <<EOF
/etc/config/meow
/etc/meow/config.yaml
EOF
cat > "$staging/control/postinst" <<'EOF'
#!/bin/sh
[ -n "${IPKG_INSTROOT}" ] && exit 0
[ -f /etc/uci-defaults/80_meow ] && sh /etc/uci-defaults/80_meow && rm -f /etc/uci-defaults/80_meow
/etc/init.d/meow enable || true
# meow-arp self-gates on the (default-off) arp_hijack section, so enabling it
# is safe: it steers nothing until clients are selected in LuCI.
/etc/init.d/meow-arp enable || true
exit 0
EOF
cat > "$staging/control/prerm" <<'EOF'
#!/bin/sh
[ -n "${IPKG_INSTROOT}" ] && exit 0
/etc/init.d/meow-arp stop 2>/dev/null
/etc/init.d/meow-arp disable 2>/dev/null
/etc/init.d/meow stop 2>/dev/null
/etc/init.d/meow disable || true
exit 0
EOF
chmod 755 "$staging/control/postinst" "$staging/control/prerm"
mkdir -p "$outdir"
pack_ipk "$staging" "${outdir}/meow_${version}_${arch}.ipk"
rm -rf "$staging"
}
build_luci() {
local version="" outdir="."
while [ $# -gt 0 ]; do
case "$1" in
--version) version="$2"; shift 2 ;;
--outdir) outdir="$2"; shift 2 ;;
*) echo "unknown option: $1" >&2; usage ;;
esac
done
[ -n "$version" ] || usage
local staging app="$SCRIPT_DIR/luci-app-meow"
staging="$(mktemp -d)"
mkdir -p "$staging/control" "$staging/data/www/luci-static" "$staging/data"
# htdocs/ maps to /www, root/ overlays / verbatim.
cp -R "$app/root/." "$staging/data/"
cp -R "$app/htdocs/luci-static/." "$staging/data/www/luci-static/"
find "$staging/data" -type d -exec chmod 755 {} +
find "$staging/data" -type f -exec chmod 644 {} +
chmod 755 "$staging/data/usr/libexec/meow-api" "$staging/data/usr/libexec/meow-validate"
cat > "$staging/control/control" <<EOF
Package: luci-app-meow
Version: ${version}
Depends: libc, luci-base, meow, curl
Section: luci
Architecture: all
Installed-Size: $(installed_size "$staging/data")
Maintainer: ${MAINTAINER}
Description: LuCI support for meow: status overview, YAML config editor,
service and transparent-proxy (gateway / side-router) settings, per-client
proxy bypass, opt-in ARP-based client steering, logs, and the built-in meow
web panel embedded in LuCI. Client steering uses the built-in unicast ARP
sender.
EOF
cat > "$staging/control/postinst" <<'EOF'
#!/bin/sh
[ -n "${IPKG_INSTROOT}" ] && exit 0
rm -f /tmp/luci-indexcache* 2>/dev/null
/etc/init.d/rpcd reload 2>/dev/null
exit 0
EOF
cat > "$staging/control/postrm" <<'EOF'
#!/bin/sh
[ -n "${IPKG_INSTROOT}" ] && exit 0
rm -f /tmp/luci-indexcache* 2>/dev/null
exit 0
EOF
chmod 755 "$staging/control/postinst" "$staging/control/postrm"
mkdir -p "$outdir"
pack_ipk "$staging" "${outdir}/luci-app-meow_${version}_all.ipk"
rm -rf "$staging"
}
case "${1:-}" in
meow) shift; build_meow "$@" ;;
luci) shift; build_luci "$@" ;;
*) usage ;;
esac
+235
View File
@@ -0,0 +1,235 @@
#!/usr/bin/env bash
# Run OpenWrt in a Docker container attached to the physical LAN (macvlan in
# bridge mode) and configure it as a side router ("bypass router") running
# meow + luci-app-meow.
#
# main router (DHCP, NAT) ── LAN ── clients
# │ │ gateway + DNS = OPENWRT_IP
# └──── OpenWrt container (OPENWRT_IP, no DHCP, meow tproxy)
#
# Usage:
# side-router.sh up [ipk-dir] create network + container, provision it,
# install *.ipk from ipk-dir (if given)
# side-router.sh install <dir> (re)install meow/luci-app-meow ipks
# side-router.sh down remove container, network and host shim
# side-router.sh status
#
# Tunables (environment):
# PARENT host NIC on the LAN (default: enP7s7)
# SUBNET LAN subnet (default: 192.168.0.0/24)
# LAN_GW main router address (default: 192.168.0.1)
# OPENWRT_IP side-router address, unused, ideally outside the main
# router's DHCP pool (default: 192.168.0.250)
# SHIM_IP host-side macvlan address, lets this host reach the container
# (macvlan children cannot talk to their parent directly)
# (default: 192.168.0.249)
# IMAGE OpenWrt rootfs image (default: openwrt/rootfs:armsr-armv8-24.10.4)
# NAME container name (default: openwrt)
set -euo pipefail
PARENT=${PARENT:-enP7s7}
SUBNET=${SUBNET:-192.168.0.0/24}
LAN_GW=${LAN_GW:-192.168.0.1}
OPENWRT_IP=${OPENWRT_IP:-192.168.0.250}
SHIM_IP=${SHIM_IP:-192.168.0.249}
IMAGE=${IMAGE:-openwrt/rootfs:armsr-armv8-24.10.4}
NAME=${NAME:-openwrt}
NET=${NAME}-lan
SHIM=${NAME}-shim
PREFIX=${SUBNET#*/}
# Host kernel modules fw4 and meow's nftables rules need; containers cannot
# load modules themselves.
MODULES="nf_tables nft_chain_nat nft_nat nft_redir nft_masq nft_ct nft_fib
nft_fib_inet nft_fib_ipv4 nft_fib_ipv6 nft_reject nft_reject_inet
nft_reject_ipv4 nft_reject_ipv6 nft_limit nft_log nft_socket nft_tproxy
nf_nat nf_conntrack"
ow() { docker exec -i "$NAME" /bin/sh -s; }
host_modules() {
local m loaded=()
for m in $MODULES; do
sudo modprobe "$m" 2>/dev/null && loaded+=("$m")
done
printf '%s\n' "${loaded[@]}" | sudo tee /etc/modules-load.d/openwrt-side-router.conf >/dev/null
}
host_shim() {
ip link show "$SHIM" >/dev/null 2>&1 && return
sudo ip link add "$SHIM" link "$PARENT" type macvlan mode bridge
sudo ip addr add "$SHIM_IP/32" dev "$SHIM"
sudo ip link set "$SHIM" up
sudo ip route add "$OPENWRT_IP/32" dev "$SHIM"
}
create() {
docker network inspect "$NET" >/dev/null 2>&1 ||
docker network create -d macvlan \
--subnet "$SUBNET" --gateway "$LAN_GW" \
--aux-address "shim=$SHIM_IP" \
-o parent="$PARENT" -o macvlan_mode=bridge "$NET"
docker container inspect "$NAME" >/dev/null 2>&1 && return
# procd needs SYS_ADMIN for its mounts; nftables needs NET_ADMIN.
docker run -d --name "$NAME" --hostname "$NAME" \
--platform linux/aarch64_generic \
--network "$NET" --ip "$OPENWRT_IP" \
--cap-add NET_ADMIN --cap-add NET_RAW --cap-add SYS_ADMIN \
--sysctl net.ipv4.ip_forward=1 \
--sysctl net.ipv4.conf.all.send_redirects=0 \
--sysctl net.ipv4.conf.default.send_redirects=0 \
--sysctl net.ipv6.conf.all.disable_ipv6=1 \
--restart unless-stopped \
"$IMAGE" /sbin/init
}
provision() {
# Wait for procd/ubus and for first-boot uci-defaults to finish, or they
# overwrite the settings below.
for _ in $(seq 1 60); do
docker exec "$NAME" sh -c 'ubus call system board && [ -z "$(ls /etc/uci-defaults)" ]' \
>/dev/null 2>&1 && break
sleep 1
done
ow <<EOF
set -e
# LAN: static address on the existing LAN, main router as gateway + DNS.
cat > /etc/config/network <<'UCI'
config interface 'loopback'
option device 'lo'
option proto 'static'
option ipaddr '127.0.0.1'
option netmask '255.0.0.0'
config interface 'lan'
option device 'eth0'
option proto 'static'
option ipaddr '$OPENWRT_IP'
option netmask '$(ipcalc_mask "$PREFIX")'
option gateway '$LAN_GW'
list dns '$LAN_GW'
UCI
# Never serve DHCP / RA on a LAN that already has a router.
uci -q batch <<'UCI'
set dhcp.lan=dhcp
set dhcp.lan.interface='lan'
set dhcp.lan.ignore='1'
set dhcp.lan.dhcpv6='disabled'
set dhcp.lan.ra='disabled'
UCI
uci -q delete dhcp.wan || true
uci commit dhcp
# Side-router firewall: accept LAN, masquerade LAN->LAN forwarding so replies
# from the main router come back through us (symmetric conntrack).
i=0
while [ "\$(uci -q get firewall.@zone[\$i].name)" != lan ]; do i=\$((i + 1)); done
lan_zone="@zone[\$i]"
uci -q batch <<UCI
set firewall.\$lan_zone.input='ACCEPT'
set firewall.\$lan_zone.output='ACCEPT'
set firewall.\$lan_zone.forward='ACCEPT'
set firewall.\$lan_zone.masq='1'
set firewall.\$lan_zone.mtu_fix='1'
UCI
uci commit firewall
uci set system.@system[0].hostname='$NAME'
uci set system.@system[0].zonename='UTC'
uci commit system
# procd sandboxes some services (dnsmasq, ...) with ujail, which cannot
# pivot_root inside Docker. Replace it with a shim that runs the command
# after "--" unjailed; the container itself is the sandbox.
if ! grep -q side-router /sbin/ujail; then
cat > /sbin/ujail <<'SHIM'
#!/bin/sh
# side-router.sh: ujail shim for containers
while [ \$# -gt 0 ] && [ "\$1" != "--" ]; do shift; done
[ "\$1" = "--" ] && shift
exec "\$@"
SHIM
chmod 755 /sbin/ujail
fi
# On shutdown `umount -a -r` remounts / read-only, which sticks to the
# container's overlay across `docker restart`.
/etc/init.d/umount disable 2>/dev/null || true
/etc/init.d/odhcpd disable 2>/dev/null || true
/etc/init.d/odhcpd stop 2>/dev/null || true
# netifd occasionally fails to claim eth0 when restarted during boot.
for try in 1 2 3 4 5; do
/etc/init.d/network restart
sleep 3
ifstatus lan | grep -q '"up": true' && break
done
/etc/init.d/dnsmasq restart
/etc/init.d/firewall restart
EOF
# LuCI (uhttpd is already in the image).
ow <<'EOF'
set -e
mkdir -p /var/lock
opkg list-installed | grep -q '^luci-base ' && exit 0
opkg update
opkg install luci luci-compat
EOF
}
# Dotted netmask for a prefix length (bash arithmetic, no ipcalc needed).
ipcalc_mask() {
local p=$1 m=$(( 0xffffffff ^ ((1 << (32 - $1)) - 1) ))
printf '%d.%d.%d.%d' $((m >> 24 & 255)) $((m >> 16 & 255)) $((m >> 8 & 255)) $((m & 255))
}
install_ipks() {
local dir=$1 f
docker exec "$NAME" rm -rf /tmp/ipk
docker exec "$NAME" mkdir -p /tmp/ipk
for f in "$dir"/meow_*.ipk "$dir"/luci-app-meow_*.ipk; do
[ -f "$f" ] && docker cp "$f" "$NAME:/tmp/ipk/"
done
ow <<'EOF'
set -e
mkdir -p /var/lock
opkg install --force-reinstall /tmp/ipk/meow_*.ipk
[ -n "$(ls /tmp/ipk/luci-app-meow_*.ipk 2>/dev/null)" ] &&
opkg install --force-reinstall /tmp/ipk/luci-app-meow_*.ipk
rm -rf /tmp/ipk /tmp/luci-*
/etc/init.d/rpcd restart
EOF
}
case "${1:-}" in
up)
host_modules
create
host_shim
provision
[ -n "${2:-}" ] && install_ipks "$2"
echo "OpenWrt side router: http://$OPENWRT_IP/ (LuCI, user root)"
;;
install)
[ -n "${2:-}" ] || { echo "usage: $0 install <ipk-dir>" >&2; exit 1; }
install_ipks "$2"
;;
down)
docker rm -f "$NAME" 2>/dev/null || true
docker network rm "$NET" 2>/dev/null || true
sudo ip link del "$SHIM" 2>/dev/null || true
;;
status)
docker ps --filter "name=^${NAME}$"
docker exec "$NAME" sh -c 'ip -4 addr show eth0; ip route; /etc/init.d/meow status; nft list tables'
;;
*)
sed -n '2,27p' "$0" | sed 's/^# \{0,1\}//'
exit 1
;;
esac
@@ -0,0 +1 @@
node_modules/
@@ -0,0 +1,62 @@
'use strict';
'require baseclass';
'require rpc';
'require fs';
'require uci';
// Shared helpers for the meow LuCI views. Runtime data comes straight from
// the meow REST API through authenticated LuCI RPC. The standalone panel
// uses the HTTP endpoint configured by the UCI panel_port and secret options.
var callServiceList = rpc.declare({
object: 'service',
method: 'list',
params: [ 'name' ],
expect: { '': {} }
});
return baseclass.extend({
apiBase: function() {
var port = uci.get('meow', 'main', 'panel_port') || '9090';
return 'http://' + window.location.hostname +
':' + port;
},
secret: function() {
return uci.get('meow', 'main', 'secret') || '';
},
// URL of the built-in web panel; the secret travels in the fragment so it
// is never sent to the server (the panel stores it in its localStorage).
panelURL: function() {
var url = this.apiBase() + '/ui';
var secret = this.secret();
return secret ? url + '#token=' + encodeURIComponent(secret) : url;
},
api: function(method, path, body) {
// LuCI RPC stays on the page's authenticated origin, including HTTPS.
// The helper connects to loopback and reads the API secret from UCI.
var args = [method, path];
if (body != null) args.push(JSON.stringify(body));
return fs.exec('/usr/libexec/meow-api', args).then(function(res) {
if (res.code !== 0)
throw new Error(res.stderr || _('Unable to reach meow API'));
return (res.stdout || '').trim() ? JSON.parse(res.stdout) : null;
});
},
serviceRunning: function() {
return L.resolveDefault(callServiceList('meow'), {}).then(function(res) {
try {
return res['meow']['instances']['meow']['running'] === true;
} catch (e) {
return false;
}
});
},
formatBytes: function(b) {
return '%1024.2mB'.format(b || 0);
}
});
@@ -0,0 +1,183 @@
'use strict';
'require baseclass';
'require fs';
'require request';
'require uci';
'require tools.meow_yaml as yaml';
// This name is also used by the package's default YAML. Other named listeners
// belong to the user; never replace the entire listeners array.
var LISTENER = 'tproxy-lan';
function port(value, label) {
if (!/^\d+$/.test(String(value)) || Number(value) < 1 || Number(value) > 65535)
throw new Error(_('Invalid port: ') + label);
return Number(value);
}
function transform(content, settings) {
var doc = yaml.parseDocument(content);
if (doc.errors.length)
throw new Error(doc.errors[0].message);
if (!yaml.isMap(doc.contents))
throw new Error(_('The YAML configuration must be a mapping.'));
var config = doc.toJS();
var panelPort = port(settings.panel_port, _('Panel port'));
var enabled = settings.enabled === '1';
var ipv6 = settings.ipv6 === '1';
var udp = settings.mode === 'tproxy';
if (enabled && settings.mode !== 'redirect' && !udp)
throw new Error(_('Unknown transparent proxy mode.'));
if (enabled && ipv6 && udp)
throw new Error(_('IPv6 capture requires REDIRECT mode; UDP TPROXY supports IPv4 only.'));
doc.set('external-controller', (settings.secret ? '0.0.0.0:' : '127.0.0.1:') + panelPort);
doc.set('secret', settings.secret || '');
var listeners = doc.get('listeners', true), managed;
if (listeners && !yaml.isSeq(listeners))
throw new Error(_('listeners must be a YAML sequence.'));
if (listeners) {
// Resolve aliases when identifying managed entries, but retain the AST
// of unrelated entries (including their comments and anchors).
listeners.items = listeners.items.filter(function(item, i) {
if (config.listeners[i] && config.listeners[i].name === LISTENER) {
managed = yaml.isMap(item) ? item : doc.createNode(config.listeners[i]);
return false;
}
return true;
});
}
if (enabled) {
var proxyPort = port(settings.tproxy_port, _('Tproxy port'));
var dnsPort = settings.dns_hijack === '1' ? port(settings.dns_port, _('DNS port')) : null;
if (proxyPort === panelPort || proxyPort === dnsPort || panelPort === dnsPort)
throw new Error(_('Panel, transparent proxy and DNS ports must be different.'));
(config.listeners || []).forEach(function(listener) {
if (listener && listener.name !== LISTENER && Number(listener.port) === proxyPort)
throw new Error(_('Transparent proxy port is already used by listener: ') + listener.name);
});
['port', 'socks-port', 'mixed-port'].forEach(function(key) {
if (Number(config[key]) === proxyPort)
throw new Error(_('Transparent proxy port is already used by ') + key);
});
if (!listeners) {
doc.set('listeners', doc.createNode([]));
listeners = doc.get('listeners', true);
}
managed = managed || doc.createNode({ name: LISTENER, 'max-connections': 4096 });
Object.entries({ type: 'tproxy', listen: ipv6 ? '::' : '0.0.0.0',
port: proxyPort, udp: udp, firewall: false }).forEach(function(entry) {
managed.set(entry[0], entry[1]);
});
listeners.add(managed);
// The shorthand installs router-local firewall rules. Replace it with
// the explicitly externally-managed gateway listener.
doc.delete('tproxy-port');
doc.set('routing-mark', 9527);
if (ipv6)
doc.set('ipv6', true);
if (dnsPort !== null) {
if (config.dns != null && (typeof config.dns !== 'object' || Array.isArray(config.dns)))
throw new Error(_('dns must be a YAML mapping.'));
// Materialize an aliased DNS mapping before editing it, avoiding
// changes to any other consumer of that anchor.
if (!yaml.isMap(doc.get('dns', true)))
doc.set('dns', doc.createNode(config.dns || {}));
// Subscriptions may resolve proxy hostnames through this DNS server.
// Moving dns.listen must move those self-references too; keeping the
// old port leaves proxy connections unable to resolve their servers.
var oldListen = config.dns && config.dns.listen;
var oldPort = typeof oldListen === 'string' && oldListen.match(/:(\d+)$/);
var proxyDns = config.dns && config.dns['proxy-server-nameserver'];
if (oldPort && Array.isArray(proxyDns)) {
proxyDns.forEach(function(server, i) {
var local = typeof server === 'string' && server.match(/^(udp:\/\/)?(127\.0\.0\.1|localhost|\[::1\])(?::(\d+))?([#?].*)?$/);
if (local && Number(local[3] || 53) === Number(oldPort[1]))
doc.setIn(['dns', 'proxy-server-nameserver', i],
(local[1] || '') + '127.0.0.1:' + dnsPort + (local[4] || ''));
});
}
doc.setIn(['dns', 'enable'], true);
doc.setIn(['dns', 'listen'], '0.0.0.0:' + dnsPort);
if (!config.dns || !config.dns.nameserver || !config.dns.nameserver.length)
doc.setIn(['dns', 'nameserver'], ['223.5.5.5', '1.1.1.1']);
}
}
return doc.toString({ lineWidth: 0 });
}
function write(path, content) {
var data = new FormData();
data.append('sessionid', L.env.sessionid);
data.append('filename', path);
data.append('filedata', new Blob([content], { type: 'text/plain' }), 'config.yaml');
return request.post(L.env.cgi_base + '/cgi-upload', data, { timeout: 0 }).then(function(res) {
if (!res.ok)
throw new Error(res.statusText || _('Upload request failed'));
var reply = res.json();
if (!reply || reply.failure)
throw new Error((reply && reply.message) || _('Upload request failed'));
});
}
function values() {
function get(section, key, fallback) {
var value = uci.get('meow', section, key);
return value == null || value === '' ? fallback : value;
}
return {
panel_port: get('main', 'panel_port', '9090'), secret: get('main', 'secret', ''),
enabled: get('tproxy', 'enabled', '0'), mode: get('tproxy', 'mode', 'tproxy'),
tproxy_port: get('tproxy', 'tproxy_port', '7893'),
dns_hijack: get('tproxy', 'dns_hijack', '1'), dns_port: get('tproxy', 'dns_port', '1053'),
ipv6: get('tproxy', 'ipv6', '0')
};
}
return baseclass.extend({
transform: transform,
prepare: function(content) { return transform(content, values()); },
// Called after form.parse(), before UCI is saved/applied. The returned
// rollback is used if the subsequent UCI save fails.
save: function() {
var path = uci.get('meow', 'main', 'config_file') || '/etc/meow/config.yaml';
if (path !== '/etc/meow/config.yaml' || (uci.get('meow', 'main', 'work_dir') || '/etc/meow') !== '/etc/meow')
return Promise.reject(new Error(_('LuCI edits require /etc/meow/config.yaml and working directory /etc/meow.')));
// getRandomValues also works on plain HTTP LuCI pages.
var token = Array.from(crypto.getRandomValues(new Uint32Array(4)), function(n) {
return n.toString(16).padStart(8, '0');
}).join('');
var scratch = '/tmp/meow-luci-settings-' + token + '.yaml';
var original, candidate;
return fs.read_direct(path).then(function(content) {
original = content;
candidate = transform(content, values());
return write(scratch, candidate);
}).then(function() {
return fs.exec('/usr/libexec/meow-validate', [token]);
}).then(function(res) {
if (res.code !== 0) {
var output = ((res.stdout || '') + (res.stderr || '')).replace(/\x1b\[[0-9;]*m/g, '');
var errors = output.split('\n').filter(function(line) { return /ERROR|Error/.test(line); });
throw new Error(errors.join('\n') || _('Configuration test failed; settings were not saved.'));
}
return fs.read_direct(path);
}).then(function(current) {
if (current !== original)
throw new Error(_('Configuration changed during validation. Reload and try again.'));
return candidate === original ? null : write(path, candidate);
}).then(function() {
return function() {
return fs.read_direct(path).then(function(current) {
if (current !== candidate)
throw new Error(_('Cannot restore YAML: configuration was changed by another session.'));
return write(path, original);
});
};
}).finally(function() {
return fs.remove(scratch).catch(function() {});
});
}
});
@@ -0,0 +1,13 @@
Copyright Eemeli Aro <eemeli@gmail.com>
Permission to use, copy, modify, and/or distribute this software for any purpose
with or without fee is hereby granted, provided that the above copyright notice
and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
THIS SOFTWARE.
@@ -0,0 +1,133 @@
'use strict';
'require baseclass';
// Generated by npm ci && npm run build in openwrt/luci-app-meow; yaml 2.9.1 (ISC).
var MeowYaml=(()=>{var vt=Object.defineProperty;var zs=Object.getOwnPropertyDescriptor;var Zs=Object.getOwnPropertyNames;var en=Object.prototype.hasOwnProperty;var tn=(s,e)=>{for(var t in e)vt(s,t,{get:e[t],enumerable:!0})},sn=(s,e,t,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of Zs(e))!en.call(s,i)&&i!==t&&vt(s,i,{get:()=>e[i],enumerable:!(n=zs(e,i))||n.enumerable});return s};var nn=s=>sn(vt({},"__esModule",{value:!0}),s);var Kn={};tn(Kn,{isMap:()=>R,isSeq:()=>F,parseDocument:()=>as});var We=Symbol.for("yaml.alias"),He=Symbol.for("yaml.document"),U=Symbol.for("yaml.map"),$t=Symbol.for("yaml.pair"),q=Symbol.for("yaml.scalar"),ne=Symbol.for("yaml.seq"),K=Symbol.for("yaml.node.type"),V=s=>!!s&&typeof s=="object"&&s[K]===We,H=s=>!!s&&typeof s=="object"&&s[K]===He,R=s=>!!s&&typeof s=="object"&&s[K]===U,T=s=>!!s&&typeof s=="object"&&s[K]===$t,E=s=>!!s&&typeof s=="object"&&s[K]===q,F=s=>!!s&&typeof s=="object"&&s[K]===ne;function L(s){if(s&&typeof s=="object")switch(s[K]){case U:case ne:return!0}return!1}function I(s){if(s&&typeof s=="object")switch(s[K]){case We:case U:case q:case ne:return!0}return!1}var Xe=s=>(E(s)||L(s))&&!!s.anchor;var j=Symbol("break visit"),cs=Symbol("skip children"),X=Symbol("remove node");function z(s,e){let t=fs(e);H(s)?we(null,s.contents,t,Object.freeze([s]))===X&&(s.contents=null):we(null,s,t,Object.freeze([]))}z.BREAK=j;z.SKIP=cs;z.REMOVE=X;function we(s,e,t,n){let i=us(s,e,t,n);if(I(i)||T(i))return hs(s,n,i),we(s,i,t,n);if(typeof i!="symbol"){if(L(e)){n=Object.freeze(n.concat(e));for(let r=0;r<e.items.length;++r){let o=we(r,e.items[r],t,n);if(typeof o=="number")r=o-1;else{if(o===j)return j;o===X&&(e.items.splice(r,1),r-=1)}}}else if(T(e)){n=Object.freeze(n.concat(e));let r=we("key",e.key,t,n);if(r===j)return j;r===X&&(e.key=null);let o=we("value",e.value,t,n);if(o===j)return j;o===X&&(e.value=null)}}return i}async function ze(s,e){let t=fs(e);H(s)?await Se(null,s.contents,t,Object.freeze([s]))===X&&(s.contents=null):await Se(null,s,t,Object.freeze([]))}ze.BREAK=j;ze.SKIP=cs;ze.REMOVE=X;async function Se(s,e,t,n){let i=await us(s,e,t,n);if(I(i)||T(i))return hs(s,n,i),Se(s,i,t,n);if(typeof i!="symbol"){if(L(e)){n=Object.freeze(n.concat(e));for(let r=0;r<e.items.length;++r){let o=await Se(r,e.items[r],t,n);if(typeof o=="number")r=o-1;else{if(o===j)return j;o===X&&(e.items.splice(r,1),r-=1)}}}else if(T(e)){n=Object.freeze(n.concat(e));let r=await Se("key",e.key,t,n);if(r===j)return j;r===X&&(e.key=null);let o=await Se("value",e.value,t,n);if(o===j)return j;o===X&&(e.value=null)}}return i}function fs(s){return typeof s=="object"&&(s.Collection||s.Node||s.Value)?Object.assign({Alias:s.Node,Map:s.Node,Scalar:s.Node,Seq:s.Node},s.Value&&{Map:s.Value,Scalar:s.Value,Seq:s.Value},s.Collection&&{Map:s.Collection,Seq:s.Collection},s):s}function us(s,e,t,n){if(typeof t=="function")return t(s,e,n);if(R(e))return t.Map?.(s,e,n);if(F(e))return t.Seq?.(s,e,n);if(T(e))return t.Pair?.(s,e,n);if(E(e))return t.Scalar?.(s,e,n);if(V(e))return t.Alias?.(s,e,n)}function hs(s,e,t){let n=e[e.length-1];if(L(n))n.items[s]=t;else if(T(n))s==="key"?n.key=t:n.value=t;else if(H(n))n.contents=t;else{let i=V(n)?"alias":"scalar";throw new Error(`Cannot replace node with ${i} parent`)}}var rn={"!":"%21",",":"%2C","[":"%5B","]":"%5D","{":"%7B","}":"%7D"},on=s=>s.replace(/[!,[\]{}]/g,e=>rn[e]),Z=class s{constructor(e,t){this.docStart=null,this.docEnd=!1,this.yaml=Object.assign({},s.defaultYaml,e),this.tags=Object.assign({},s.defaultTags,t)}clone(){let e=new s(this.yaml,this.tags);return e.docStart=this.docStart,e}atDocument(){let e=new s(this.yaml,this.tags);switch(this.yaml.version){case"1.1":this.atNextDocument=!0;break;case"1.2":this.atNextDocument=!1,this.yaml={explicit:s.defaultYaml.explicit,version:"1.2"},this.tags=Object.assign({},s.defaultTags);break}return e}add(e,t){this.atNextDocument&&(this.yaml={explicit:s.defaultYaml.explicit,version:"1.1"},this.tags=Object.assign({},s.defaultTags),this.atNextDocument=!1);let n=e.trim().split(/[ \t]+/),i=n.shift();switch(i){case"%TAG":{if(n.length!==2&&(t(0,"%TAG directive should contain exactly two parts"),n.length<2))return!1;let[r,o]=n;return this.tags[r]=o,!0}case"%YAML":{if(this.yaml.explicit=!0,n.length!==1)return t(0,"%YAML directive should contain exactly one part"),!1;let[r]=n;if(r==="1.1"||r==="1.2")return this.yaml.version=r,!0;{let o=/^\d+\.\d+$/.test(r);return t(6,`Unsupported YAML version ${r}`,o),!1}}default:return t(0,`Unknown directive ${i}`,!0),!1}}tagName(e,t){if(e==="!")return"!";if(e[0]!=="!")return t(`Not a valid tag: ${e}`),null;if(e[1]==="<"){let o=e.slice(2,-1);return o==="!"||o==="!!"?(t(`Verbatim tags aren't resolved, so ${e} is invalid.`),null):(e[e.length-1]!==">"&&t("Verbatim tags must end with a >"),o)}let[,n,i]=e.match(/^(.*!)([^!]*)$/s);i||t(`The ${e} tag has no suffix`);let r=this.tags[n];if(r)try{return r+decodeURIComponent(i)}catch(o){return t(String(o)),null}return n==="!"?e:(t(`Could not resolve tag: ${e}`),null)}tagStLine truncated
`)}};Z.defaultYaml={explicit:!1,version:"1.2"};Z.defaultTags={"!!":"tag:yaml.org,2002:"};function Ze(s){if(/[\x00-\x19\s,[\]{}]/.test(s)){let t=`Anchor must not contain whitespace or control characters: ${JSON.stringify(s)}`;throw new Error(t)}return!0}function _t(s){let e=new Set;return z(s,{Value(t,n){n.anchor&&e.add(n.anchor)}}),e}function Pt(s,e){for(let t=1;;++t){let n=`${s}${t}`;if(!e.has(n))return n}}function ps(s,e){let t=[],n=new Map,i=null;return{onAnchor:r=>{t.push(r),i??(i=_t(s));let o=Pt(e,i);return i.add(o),o},setAnchors:()=>{for(let r of t){let o=n.get(r);if(typeof o=="object"&&o.anchor&&(E(o.node)||L(o.node)))o.node.anchor=o.anchor;else{let a=new Error("Failed to resolve repeated object (this should not happen)");throw a.source=r,a}}},sourceObjects:n}}function le(s,e,t,n){if(n&&typeof n=="object")if(Array.isArray(n))for(let i=0,r=n.length;i<r;++i){let o=n[i],a=le(s,n,String(i),o);a===void 0?delete n[i]:a!==o&&(n[i]=a)}else if(n instanceof Map)for(let i of Array.from(n.keys())){let r=n.get(i),o=le(s,n,i,r);o===void 0?n.delete(i):o!==r&&n.set(i,o)}else if(n instanceof Set)for(let i of Array.from(n)){let r=le(s,n,i,i);r===void 0?n.delete(i):r!==i&&(n.delete(i),n.add(r))}else for(let[i,r]of Object.entries(n)){let o=le(s,n,i,r);o===void 0?delete n[i]:o!==r&&(n[i]=o)}return s.call(e,t,n)}function M(s,e,t){if(Array.isArray(s))return s.map((n,i)=>M(n,String(i),t));if(s&&typeof s.toJSON=="function"){if(!t||!Xe(s))return s.toJSON(e,t);let n={aliasCount:0,count:1,res:void 0};t.anchors.set(s,n),t.onCreate=r=>{n.res=r,delete t.onCreate};let i=s.toJSON(e,t);return t.onCreate&&t.onCreate(i),i}return typeof s=="bigint"&&!t?.keep?Number(s):s}var ce=class{constructor(e){Object.defineProperty(this,K,{value:e})}clone(){let e=Object.create(Object.getPrototypeOf(this),Object.getOwnPropertyDescriptors(this));return this.range&&(e.range=this.range.slice()),e}toJS(e,{mapAsMap:t,maxAliasCount:n,onAnchor:i,reviver:r}={}){if(!H(e))throw new TypeError("A document argument is required");let o={anchors:new Map,doc:e,keep:!0,mapAsMap:t===!0,mapKeyWarned:!1,maxAliasCount:typeof n=="number"?n:100},a=M(this,"",o);if(typeof i=="function")for(let{count:l,res:c}of o.anchors.values())i(c,l);return typeof r=="function"?le(r,{"":a},"",a):a}};var ie=class extends ce{constructor(e){super(We),this.source=e,Object.defineProperty(this,"tag",{set(){throw new Error("Alias nodes cannot have tags")}})}resolve(e,t){if(t?.maxAliasCount===0)throw new ReferenceError("Alias resolution is disabled");let n;t?.aliasResolveCache?n=t.aliasResolveCache:(n=[],z(e,{Node:(r,o)=>{(V(o)||Xe(o))&&n.push(o)}}),t&&(t.aliasResolveCache=n));let i;for(let r of n){if(r===this)break;r.anchor===this.source&&(i=r)}if(i&&t){let{anchors:r,doc:o,maxAliasCount:a}=t,l=r.get(i);if(l||(M(i,null,t),l=r.get(i)),l?.res===void 0){let c="This should not happen: Alias anchor was not resolved?";throw new ReferenceError(c)}if(a>=0&&(l.count+=1,l.aliasCount===0&&(l.aliasCount=et(o,i,r)),l.count*l.aliasCount>a)){let c="Excessive alias count indicates a resource exhaustion attack";throw new ReferenceError(c)}}return i}toJSON(e,t){if(!t)return{source:this.source};let n=this.resolve(t.doc,t);if(!n){let i=`Unresolved alias (the anchor must be set before the alias): ${this.source}`;throw new ReferenceError(i)}return t.anchors.get(n).res}toString(e,t,n){let i=`*${this.source}`;if(e){if(Ze(this.source),e.options.verifyAliasOrder&&!e.anchors.has(this.source)){let r=`Unresolved alias (the anchor must be set before the alias): ${this.source}`;throw new Error(r)}if(e.implicitKey)return`${i} `}return i}};function et(s,e,t){if(V(e)){let n=e.resolve(s),i=t&&n&&t.get(n);return i?i.count*i.aliasCount:0}else if(L(e)){let n=0;for(let i of e.items){let r=et(s,i,t);r>n&&(n=r)}return n}else if(T(e)){let n=et(s,e.key,t),i=et(s,e.value,t);return Math.max(n,i)}return 1}var tt=s=>!s||typeof s!="function"&&typeof s!="object",y=class extends ce{constructor(e){super(q),this.value=e}toJSON(e,t){return t?.keep?this.value:M(this.value,e,t)}toString(){return String(this.value)}};y.BLOCK_FOLDED="BLOCK_FOLDED";y.BLOCK_LITERAL="BLOCK_LITERAL";y.PLAIN="PLAIN";y.QUOTE_DOUBLE="QUOTE_DOUBLE";y.QUOTE_SINGLE="QUOTE_SINGLE";var an="tag:yaml.org,2002:";function ln(s,e,t){if(e){let n=t.filter(r=>r.tag===e),i=n.find(r=>!r.format)??n[0];if(!i)throw new Error(`Tag ${e} not found`);return i}return t.find(n=>n.identify?.(s)&&!n.format)}function re(s,e,t){if(H(s)&&(s=s.contents),I(s))return s;if(T(s)){let f=t.schema[U].createNode?.(t.schema,null,t);return f.items.push(s),f}(s instanceof String||s instanceof Number||s instanceof Boolean||typeof BigInt<"u"&&s instanceof BigInt)&&(s=s.valueOf());let{aliasDuplicateObjects:n,onAnchor:i,onTagObj:r,schema:o,sourceObjects:a}=t,l;if(n&&s&&typeof s=="object"){if(l=a.get(s),l)return l.anchor??(l.anchor=i(s)),new ie(l.anchor);l={anchor:null,node:null},a.set(s,l)}e?.startsWith("!!")&&(e=an+e.slice(2));let c=ln(s,e,o.tags);if(!c){if(s&&typeof s.toJSON=="function"&&(s=s.toJSON()),!s||typeof s!=" Line truncated
`)?x(t,e):t.includes(`
`)?`
`+x(t,e):(s.endsWith(" ")?"":" ")+t;var Mt="flow",st="block",$e="quoted";function _e(s,e,t="flow",{indentAtStart:n,lineWidth:i=80,minContentWidth:r=20,onFold:o,onOverflow:a}={}){if(!i||i<0)return s;i<r&&(r=0);let l=Math.max(1+r,1+i-e.length);if(s.length<=l)return s;let c=[],p={},f=i-e.length;typeof n=="number"&&(n>i-Math.max(2,r)?c.push(0):f=i-n);let u,d,g=!1,h=-1,m=-1,w=-1;t===st&&(h=ds(s,h,e.length),h!==-1&&(f=h+l));for(let k;k=s[h+=1];){if(t===$e&&k==="\\"){switch(m=h,s[h+1]){case"x":h+=3;break;case"u":h+=5;break;case"U":h+=9;break;default:h+=1}w=h}if(k===`
`)t===st&&(h=ds(s,h,e.length)),f=h+e.length+l,u=void 0;else{if(k===" "&&d&&d!==" "&&d!==`
`&&d!==" "){let N=s[h+1];N&&N!==" "&&N!==`
`&&N!==" "&&(u=h)}if(h>=f)if(u)c.push(u),f=u+l,u=void 0;else if(t===$e){for(;d===" "||d===" ";)d=k,k=s[h+=1],g=!0;let N=h>w+1?h-2:m-1;if(p[N])return s;c.push(N),p[N]=!0,f=N+l,u=void 0}else g=!0}d=k}if(g&&a&&a(),c.length===0)return s;o&&o();let S=s.slice(0,c[0]);for(let k=0;k<c.length;++k){let N=c[k],O=c[k+1]||s.length;N===0?S=`
${e}${s.slice(0,O)}`:(t===$e&&p[N]&&(S+=`${s[N]}\\`),S+=`
${e}${s.slice(N+1,O)}`)}return S}function ds(s,e,t){let n=e,i=e+1,r=s[i];for(;r===" "||r===" ";)if(e<i+t)r=s[++e];else{do r=s[++e];while(r&&r!==`
`);n=e,i=e+1,r=s[i]}return n}var it=(s,e)=>({indentAtStart:e?s.indent.length:s.indentAtStart,lineWidth:s.options.lineWidth,minContentWidth:s.options.minContentWidth}),rt=s=>/^(%|---|\.\.\.)/m.test(s);function cn(s,e,t){if(!e||e<0)return!1;let n=e-t,i=s.length;if(i<=n)return!1;for(let r=0,o=0;r<i;++r)if(s[r]===`
`){if(r-o>n)return!0;if(o=r+1,i-o<=n)return!1}return!0}function Pe(s,e){let t=JSON.stringify(s);if(e.options.doubleQuotedAsJSON)return t;let{implicitKey:n}=e,i=e.options.doubleQuotedMinMultiLineLength,r=e.indent||(rt(s)?" ":""),o="",a=0;for(let l=0,c=t[l];c;c=t[++l])if(c===" "&&t[l+1]==="\\"&&t[l+2]==="n"&&(o+=t.slice(a,l)+"\\ ",l+=1,a=l,c="\\"),c==="\\")switch(t[l+1]){case"u":{o+=t.slice(a,l);let p=t.substr(l+2,4);switch(p){case"0000":o+="\\0";break;case"0007":o+="\\a";break;case"000b":o+="\\v";break;case"001b":o+="\\e";break;case"0085":o+="\\N";break;case"00a0":o+="\\_";break;case"2028":o+="\\L";break;case"2029":o+="\\P";break;default:p.substr(0,2)==="00"?o+="\\x"+p.substr(2):o+=t.substr(l,6)}l+=5,a=l+1}break;case"n":if(n||t[l+2]==='"'||t.length<i)l+=1;else{for(o+=t.slice(a,l)+`
`;t[l+2]==="\\"&&t[l+3]==="n"&&t[l+4]!=='"';)o+=`
`,l+=2;o+=r,t[l+2]===" "&&(o+="\\"),l+=1,a=l+1}break;default:l+=1}return o=a?o+t.slice(a):t,n?o:_e(o,r,$e,it(e,!1))}function Bt(s,e){if(e.options.singleQuote===!1||e.implicitKey&&s.includes(`
`)||/[ \t]\n|\n[ \t]/.test(s))return Pe(s,e);let t=e.indent||(rt(s)?" ":""),n="'"+s.replace(/'/g,"''").replace(/\n+/g,`$&
${t}`)+"'";return e.implicitKey?n:_e(n,t,Mt,it(e,!1))}function Oe(s,e){let{singleQuote:t}=e.options,n;if(t===!1)n=Pe;else{let i=s.includes('"'),r=s.includes("'");i&&!r?n=Bt:r&&!i?n=Pe:n=t?Bt:Pe}return n(s,e)}var Dt;try{Dt=new RegExp(`(^|(?<!
))
+(?!
|$)`,"g")}catch{Dt=/\n+(?!\n|$)/g}function nt({comment:s,type:e,value:t},n,i,r){let{blockQuote:o,commentString:a,lineWidth:l}=n.options;if(!o||/\n[\t ]+$/.test(t))return Oe(t,n);let c=n.indent||(n.forceBlockIndent||rt(t)?" ":""),p=o==="literal"?!0:o==="folded"||e===y.BLOCK_FOLDED?!1:e===y.BLOCK_LITERAL?!0:!cn(t,l,c.length);if(!t)return p?`|
`:`>
`;let f,u;for(u=t.length;u>0;--u){let O=t[u-1];if(O!==`
`&&O!==" "&&O!==" ")break}let d=t.substring(u),g=d.indexOf(`
`);g===-1?f="-":t===d||g!==d.length-1?(f="+",r&&r()):f="",d&&(t=t.slice(0,-d.length),d[d.length-1]===`
`&&(d=d.slice(0,-1)),d=d.replace(Dt,`$&${c}`));let h=!1,m,w=-1;for(m=0;m<t.length;++m){let O=t[m];if(O===" ")h=!0;else if(O===`
`)w=m;else break}let S=t.substring(0,w<m?w+1:m);S&&(t=t.substring(S.length),S=S.replace(/\n+/g,`$&${c}`));let N=(h?c?"2":"1":"")+f;if(s&&(N+=" "+a(s.replace(/ ?[\r\n]+/g," ")),i&&i()),!p){let O=t.replace(/\n+/g,`
$&`).replace(/(?:^|\n)([\t ].*)(?:([\n\t ]*)\n(?![\n\t ]))?/g,"$1$2").replace(/\n+/g,`$&${c}`),A=!1,C=it(n,!0);o!=="folded"&&e!==y.BLOCK_FOLDED&&(C.onOverflow=()=>{A=!0});let b=_e(`${S}${O}${d}`,c,st,C);if(!A)return`>${N}
${c}${b}`}return t=t.replace(/\n+/g,`$&${c}`),`|${N}
${c}${S}${t}${d}`}function fn(s,e,t,n){let{type:i,value:r}=s,{actualString:o,implicitKey:a,indent:l,indentStep:c,inFlow:p}=e;if(a&&r.includes(`
`)||p&&/[[\]{},]/.test(r))return Oe(r,e);if(/^[\n\t ,[\]{}#&*!|>'"%@`]|^[?-]$|^[?-][ \t]|[\n:][ \t]|[ \t]\n|[\n\t ]#|[\n\t :]$/.test(r))return a||p||!r.includes(`
`)?Oe(r,e):nt(s,e,t,n);if(!a&&!p&&i!==y.PLAIN&&r.includes(`
`))return nt(s,e,t,n);if(rt(r)){if(l==="")return e.forceBlockIndent=!0,nt(s,e,t,n);if(a&&l===c)return Oe(r,e)}let f=r.replace(/\n+/g,`$&
${l}`);if(o){let u=h=>h.default&&h.tag!=="tag:yaml.org,2002:str"&&h.test?.test(f),{compat:d,tags:g}=e.doc.schema;if(g.some(u)||d?.some(u))return Oe(r,e)}return a?f:_e(f,l,Mt,it(e,!1))}function de(s,e,t,n){let{implicitKey:i,inFlow:r}=e,o=typeof s.value=="string"?s:Object.assign({},s,{value:String(s.value)}),{type:a}=s;a!==y.QUOTE_DOUBLE&&/[\x00-\x08\x0b-\x1f\x7f-\x9f\u{D800}-\u{DFFF}]/u.test(o.value)&&(a=y.QUOTE_DOUBLE);let l=p=>{switch(p){case y.BLOCK_FOLDED:case y.BLOCK_LITERAL:return i||r?Oe(o.value,e):nt(o,e,t,n);case y.QUOTE_DOUBLE:return Pe(o.value,e);case y.QUOTE_SINGLE:return Bt(o.value,e);case y.PLAIN:return fn(o,e,t,n);default:return null}},c=l(a);if(c===null){let{defaultKeyType:p,defaultStringType:f}=e.options,u=i&&p||f;if(c=l(u),c===null)throw new Error(`Unsupported default string type ${u}`)}return c}function ot(s,e){let t=Object.assign({blockQuote:!0,commentString:ms,defaultKeyType:null,defaultStringType:"PLAIN",directives:null,doubleQuotedAsJSON:!1,doubleQuotedMinMultiLineLength:40,falseStr:"false",flowCollectionPadding:!0,indentSeq:!0,lineWidth:80,minContentWidth:20,nullStr:"null",simpleKeys:!1,singleQuote:null,trailingComma:!1,trueStr:"true",verifyAliasOrder:!0},s.schema.toStringOptions,e),n;switch(t.collectionStyle){case"block":n=!1;break;case"flow":n=!0;break;default:n=null}return{anchors:new Set,doc:s,flowCollectionPadding:t.flowCollectionPadding?" ":"",indent:"",indentStep:typeof t.indent=="number"?" ".repeat(t.indent):" ",inFlow:n,options:t}}function un(s,e){if(e.tag){let i=s.filter(r=>r.tag===e.tag);if(i.length>0)return i.find(r=>r.format===e.format)??i[0]}let t,n;if(E(e)){n=e.value;let i=s.filter(r=>r.identify?.(n));if(i.length>1){let r=i.filter(o=>o.test);r.length>0&&(i=r)}t=i.find(r=>r.format===e.format)??i.find(r=>!r.format)}else n=e,t=s.find(i=>i.nodeClass&&n instanceof i.nodeClass);if(!t){let i=n?.constructor?.name??(n===null?"null":typeof n);throw new Error(`Tag not resolved for ${i} value`)}return t}function hn(s,e,{anchors:t,doc:n}){if(!n.directives)return"";let i=[],r=(E(s)||L(s))&&s.anchor;r&&Ze(r)&&(t.add(r),i.push(`&${r}`));let o=s.tag??(e.default?null:e.tag);return o&&i.push(n.directives.tagString(o)),i.join(" ")}function oe(s,e,t,n){if(T(s))return s.toString(e,t,n);if(V(s)){if(e.doc.directives)return s.toString(e);if(e.resolvedAliases?.has(s))throw new TypeError("Cannot stringify circular structure without alias nodes");e.resolvedAliases?e.resolvedAliases.add(s):e.resolvedAliases=new Set([s]),s=s.resolve(e.doc)}let i,r=I(s)?s:e.doc.createNode(s,{onTagObj:l=>i=l});i??(i=un(e.doc.schema.tags,r));let o=hn(r,i,e);o.length>0&&(e.indentAtStart=(e.indentAtStart??0)+o.length+1);let a=typeof i.stringify=="function"?i.stringify(r,e,t,n):E(r)?de(r,e,t,n):r.toString(e,t,n);return o?E(r)||a[0]==="{"||a[0]==="["?`${o} ${a}`:`${o}
${e.indent}${a}`:a}function gs({key:s,value:e},t,n,i){let{allNullValues:r,doc:o,indent:a,indentStep:l,options:{commentString:c,indentSeq:p,simpleKeys:f}}=t,u=I(s)&&s.comment||null;if(f){if(u)throw new Error("With simple keys, key nodes cannot have comments");if(L(s)||!I(s)&&typeof s=="object"){let C="With simple keys, collection cannot be used as a key value";throw new Error(C)}}let d=!f&&(!s||u&&e==null&&!t.inFlow||L(s)||(E(s)?s.type===y.BLOCK_FOLDED||s.type===y.BLOCK_LITERAL:typeof s=="object"));t=Object.assign({},t,{allNullValues:!1,implicitKey:!d&&(f||!r),indent:a+l});let g=!1,h=!1,m=oe(s,t,()=>g=!0,()=>h=!0);if(!d&&!t.inFlow&&m.length>1024){if(f)throw new Error("With simple keys, single line scalar must not span more than 1024 characters");d=!0}if(t.inFlow){if(r||e==null)return g&&n&&n(),m===""?"?":d?`? ${m}`:m}else if(r&&!f||e==null&&d)return m=`? ${m}`,u&&!g?m+=ee(m,t.indent,c(u)):h&&i&&i(),m;g&&(u=null),d?(u&&(m+=ee(m,t.indent,c(u))),m=`? ${m}
${a}:`):(m=`${m}:`,u&&(m+=ee(m,t.indent,c(u))));let w,S,k;I(e)?(w=!!e.spaceBefore,S=e.commentBefore,k=e.comment):(w=!1,S=null,k=null,e&&typeof e=="object"&&(e=o.createNode(e))),t.implicitKey=!1,!d&&!u&&E(e)&&(t.indentAtStart=m.length+1),h=!1,!p&&l.length>=2&&!t.inFlow&&!d&&F(e)&&!e.flow&&!e.tag&&!e.anchor&&(t.indent=t.indent.substring(2));let N=!1,O=oe(e,t,()=>N=!0,()=>h=!0),A=" ";if(u||w||S){if(A=w?`
`:"",S){let C=c(S);A+=`
${x(C,t.indent)}`}O===""&&!t.inFlow?A===`
`&&k&&(A=`
`):A+=`
${t.indent}`}else if(!d&&L(e)){let C=O[0],b=O.indexOf(`
`),_=b!==-1,ae=t.inFlow??e.flow??e.items.length===0;if(_||!ae){let be=!1;if(_&&(C==="&"||C==="!")){let P=O.indexOf(" ");C==="&"&&P!==-1&&P<b&&O[P+1]==="!"&&(P=O.indexOf(" ",P+1)),(P===-1||b<P)&&(be=!0)}be||(A=`
${t.indent}`)}}else(O===""||O[0]===`
`)&&(A="");return m+=A+O,t.inFlow?N&&n&&n():k&&!N?m+=ee(m,t.indent,c(k)):h&&i&&i(),m}function Kt(s,e){(s==="debug"||s==="warn")&&console.warn(e)}var at="<<",J={identify:s=>s===at||typeof s=="symbol"&&s.description===at,default:"key",tag:"tag:yaml.org,2002:merge",test:/^<<$/,resolve:()=>Object.assign(new y(Symbol(at)),{addToJSMap:qt}),stringify:()=>at},ys=(s,e)=>(J.identify(e)||E(e)&&(!e.type||e.type===y.PLAIN)&&J.identify(e.value))&&s?.doc.schema.tags.some(t=>t.tag===J.tag&&t.default);function qt(s,e,t){let n=bs(s,t);if(F(n))for(let i of n.items)jt(s,e,i);else if(Array.isArray(n))for(let i of n)jt(s,e,i);else jt(s,e,n)}function jt(s,e,t){let n=bs(s,t);if(!R(n))throw new Error("Merge sources must be maps or map aliases");let i=n.toJSON(null,s,Map);for(let[r,o]of i)e instanceof Map?e.has(r)||e.set(r,o):e instanceof Set?e.add(r):Object.prototype.hasOwnProperty.call(e,r)||Object.defineProperty(e,r,{value:o,writable:!0,enumerable:!0,configurable:!0});return e}function bs(s,e){return s&&V(e)?e.resolve(s.doc,s):e}function lt(s,e,{key:t,value:n}){if(I(t)&&t.addToJSMap)t.addToJSMap(s,e,n);else if(ys(s,t))qt(s,e,n);else{let i=M(t,"",s);if(e instanceof Map)e.set(i,M(n,i,s));else if(e instanceof Set)e.add(i);else{let r=pn(t,i,s),o=M(n,r,s);r in e?Object.defineProperty(e,r,{value:o,writable:!0,enumerable:!0,configurable:!0}):e[r]=o}}return e}function pn(s,e,t){if(e===null)return"";if(typeof e!="object")return String(e);if(I(s)&&t?.doc){let n=ot(t.doc,{});n.anchors=new Set;for(let r of t.anchors.keys())n.anchors.add(r.anchor);n.inFlow=!0,n.inStringifyKey=!0;let i=s.toString(n);if(!t.mapKeyWarned){let r=JSON.stringify(i);r.length>40&&(r=r.substring(0,36)+'..."'),Kt(t.doc.options.logLevel,`Keys with collection values will be stringified due to JS Object restrictions: ${r}. Set mapAsMap: true to use object keys.`),t.mapKeyWarned=!0}return i}return JSON.stringify(e)}function Ae(s,e,t){let n=re(s,void 0,t),i=re(e,void 0,t);return new v(n,i)}var v=class s{constructor(e,t=null){Object.defineProperty(this,K,{value:$t}),this.key=e,this.value=t}clone(e){let{key:t,value:n}=this;return I(t)&&(t=t.clone(e)),I(n)&&(n=n.clone(e)),new s(t,n)}toJSON(e,t){let n=t?.mapAsMap?new Map:{};return lt(t,n,this)}toString(e,t,n){return e?.doc?gs(this,e,t,n):JSON.stringify(this)}};function ft(s,e,t){return(e.inFlow??s.flow?dn:mn)(s,e,t)}function mn({comment:s,items:e},t,{blockItemPrefix:n,flowChars:i,itemIndent:r,onChompKeep:o,onComment:a}){let{indent:l,options:{commentString:c}}=t,p=Object.assign({},t,{indent:r,type:null}),f=!1,u=[];for(let g=0;g<e.length;++g){let h=e[g],m=null;if(I(h))!f&&h.spaceBefore&&u.push(""),ct(t,u,h.commentBefore,f),h.comment&&(m=h.comment);else if(T(h)){let S=I(h.key)?h.key:null;S&&(!f&&S.spaceBefore&&u.push(""),ct(t,u,S.commentBefore,f))}f=!1;let w=oe(h,p,()=>m=null,()=>f=!0);m&&(w+=ee(w,r,c(m))),f&&m&&(f=!1),u.push(n+w)}let d;if(u.length===0)d=i.start+i.end;else{d=u[0];for(let g=1;g<u.length;++g){let h=u[g];d+=h?`
${l}${h}`:`
`}}return s?(d+=`
`+x(c(s),l),a&&a()):f&&o&&o(),d}function dn({items:s},e,{flowChars:t,itemIndent:n}){let{indent:i,indentStep:r,flowCollectionPadding:o,options:{commentString:a}}=e;n+=r;let l=Object.assign({},e,{indent:n,inFlow:!0,type:null}),c=!1,p=0,f=[];for(let g=0;g<s.length;++g){let h=s[g],m=null;if(I(h))h.spaceBefore&&f.push(""),ct(e,f,h.commentBefore,!1),h.comment&&(m=h.comment);else if(T(h)){let S=I(h.key)?h.key:null;S&&(S.spaceBefore&&f.push(""),ct(e,f,S.commentBefore,!1),S.comment&&(c=!0));let k=I(h.value)?h.value:null;k?(k.comment&&(m=k.comment),k.commentBefore&&(c=!0)):h.value==null&&S?.comment&&(m=S.comment)}m&&(c=!0);let w=oe(h,l,()=>m=null);c||(c=f.length>p||w.includes(`
`)),g<s.length-1?w+=",":e.options.trailingComma&&(e.options.lineWidth>0&&(c||(c=f.reduce((S,k)=>S+k.length+2,2)+(w.length+2)>e.options.lineWidth)),c&&(w+=",")),m&&(w+=ee(w,n,a(m))),f.push(w),p=f.length}let{start:u,end:d}=t;if(f.length===0)return u+d;if(!c){let g=f.reduce((h,m)=>h+m.length+2,2);c=e.options.lineWidth>0&&g>e.options.lineWidth}if(c){let g=u;for(let h of f)g+=h?`
${r}${i}${h}`:`
`;return`${g}
${i}${d}`}else return`${u}${o}${f.join(" ")}${o}${d}`}function ct({indent:s,options:{commentString:e}},t,n,i){if(n&&i&&(n=n.replace(/^\n+/,"")),n){let r=x(e(n),s);t.push(r.trimStart())}}function fe(s,e){let t=E(e)?e.value:e;for(let n of s)if(T(n)&&(n.key===e||n.key===t||E(n.key)&&n.key.value===t))return n}var $=class extends ke{static get tagName(){return"tag:yaml.org,2002:map"}constructor(e){super(U,e),this.items=[]}static from(e,t,n){let{keepUndefined:i,replacer:r}=n,o=new this(e),a=(l,c)=>{if(typeof r=="function")c=r.call(t,l,c);else if(Array.isArray(r)&&!r.includes(l))return;(c!==void 0||i)&&o.items.push(Ae(l,c,n))};if(t instanceof Map)for(let[l,c]of t)a(l,c);else if(t&&typeof t=="object")for(let l of Object.keys(t))a(l,t[l]);return typeof e.sortMapEntries=="function"&&o.items.sort(e.sortMapEntries),o}add(e,t){let n;T(e)?n=e:!e||typeof e!="object"||!("key"in e)?n=new v(e,e?.value):n=new v(e.key,e.value);let i=fe(this.items,n.key),r=this.schema?.sortMapEntries;if(i){if(!t)throw new Error(`Key ${n.key} already set`);E(i.value)&&tt(n.value)?i.value.value=n.value:i.value=n.value}else if(r){let o=this.items.findIndex(a=>r(n,a)<0);o===-1?this.items.push(n):this.items.splice(o,0,n)}else this.items.push(n)}delete(e){let t=fe(this.items,e);return t?this.items.splice(this.items.indexOf(t),1).length>0:!1}get(e,t){let i=fe(this.items,e)?.value;return(!t&&E(i)?i.value:i)??void 0}has(e){return!!fe(this.items,e)}set(e,t){this.add(new v(e,t),!0)}toJSON(e,t,n){let i=n?new n:t?.mapAsMap?new Map:{};t?.onCreate&&t.onCreate(i);for(let r of this.items)lt(t,i,r);return i}toString(e,t,n){if(!e)return JSON.stringify(this);for(let i of this.items)if(!T(i))throw new Error(`Map items must all be pairs; found ${JSON.stringify(i)} instead`);return!e.allNullValues&&this.hasAllNullValues(!1)&&(e=Object.assign({},e,{allNullValues:!0})),ft(this,e,{blockItemPrefix:"",flowChars:{start:"{",end:"}"},itemIndent:e.indent||"",onChompKeep:n,onComment:t})}};var Y={collection:"map",default:!0,nodeClass:$,tag:"tag:yaml.org,2002:map",resolve(s,e){return R(s)||e("Expected a mapping for this tag"),s},createNode:(s,e,t)=>$.from(s,e,t)};var B=class extends ke{static get tagName(){return"tag:yaml.org,2002:seq"}constructor(e){super(ne,e),this.items=[]}add(e){this.items.push(e)}delete(e){let t=ut(e);return typeof t!="number"?!1:this.items.splice(t,1).length>0}get(e,t){let n=ut(e);if(typeof n!="number")return;let i=this.items[n];return!t&&E(i)?i.value:i}has(e){let t=ut(e);return typeof t=="number"&&t<this.items.length}set(e,t){let n=ut(e);if(typeof n!="number")throw new Error(`Expected a valid index, not ${e}.`);let i=this.items[n];E(i)&&tt(t)?i.value=t:this.items[n]=t}toJSON(e,t){let n=[];t?.onCreate&&t.onCreate(n);let i=0;for(let r of this.items)n.push(M(r,String(i++),t));return n}toString(e,t,n){return e?ft(this,e,{blockItemPrefix:"- ",flowChars:{start:"[",end:"]"},itemIndent:(e.indent||"")+" ",onChompKeep:n,onComment:t}):JSON.stringify(this)}static from(e,t,n){let{replacer:i}=n,r=new this(e);if(t&&Symbol.iterator in Object(t)){let o=0;for(let a of t){if(typeof i=="function"){let l=t instanceof Set?a:String(o++);a=i.call(t,l,a)}r.items.push(re(a,void 0,n))}}return r}};function ut(s){let e=E(s)?s.value:s;return e&&typeof e=="string"&&(e=Number(e)),typeof e=="number"&&Number.isInteger(e)&&e>=0?e:null}var Q={collection:"seq",default:!0,nodeClass:B,tag:"tag:yaml.org,2002:seq",resolve(s,e){return F(s)||e("Expected a sequence for this tag"),s},createNode:(s,e,t)=>B.from(s,e,t)};var ue={identify:s=>typeof s=="string",default:!0,tag:"tag:yaml.org,2002:str",resolve:s=>s,stringify(s,e,t,n){return e=Object.assign({actualString:!0},e),de(s,e,t,n)}};var ge={identify:s=>s==null,createNode:()=>new y(null),default:!0,tag:"tag:yaml.org,2002:null",test:/^(?:~|[Nn]ull|NULL)?$/,resolve:()=>new y(null),stringify:({source:s},e)=>typeof s=="string"&&ge.test.test(s)?s:e.options.nullStr};var Me={identify:s=>typeof s=="boolean",default:!0,tag:"tag:yaml.org,2002:bool",test:/^(?:[Tt]rue|TRUE|[Ff]alse|FALSE)$/,resolve:s=>new y(s[0]==="t"||s[0]==="T"),stringify({source:s,value:e},t){if(s&&Me.test.test(s)){let n=s[0]==="t"||s[0]==="T";if(e===n)return s}return e?t.options.trueStr:t.options.falseStr}};function D({format:s,minFractionDigits:e,tag:t,value:n}){if(typeof n=="bigint")return String(n);let i=typeof n=="number"?n:Number(n);if(!isFinite(i))return isNaN(i)?".nan":i<0?"-.inf":".inf";let r=Object.is(n,-0)?"-0":JSON.stringify(n);if(!s&&e&&(!t||t==="tag:yaml.org,2002:float")&&/^-?\d/.test(r)&&!r.includes("e")){let o=r.indexOf(".");o<0&&(o=r.length,r+=".");let a=e-(r.length-o-1);for(;a-- >0;)r+="0"}return r}var ht={identify:s=>typeof s=="number",default:!0,tag:"tag:yaml.org,2002:float",test:/^(?:[-+]?\.(?:inf|Inf|INF)|\.nan|\.NaN|\.NAN)$/,resolve:s=>s.slice(-3).toLowerCase()==="nan"?NaN:s[0]==="-"?Number.NEGATIVE_INFINITY:Number.POSITIVE_INFINITY,stringify:D},pt={identify:s=>typeof s=="number",default:!0,tag:"tag:yaml.org,2002:float",format:"EXP",test:/^[-+]?(?:\.[0-9]+|[0-9]+(?:\.Line truncated
`:" ")}return de({comment:s,type:e,value:a},n,i,r)}};function Ft(s,e){if(F(s))for(let t=0;t<s.items.length;++t){let n=s.items[t];if(!T(n)){if(R(n)){n.items.length>1&&e("Each pair must have its own sequence indicator");let i=n.items[0]||new v(new y(null));if(n.commentBefore&&(i.key.commentBefore=i.key.commentBefore?`${n.commentBefore}
${i.key.commentBefore}`:n.commentBefore),n.comment){let r=i.value??i.key;r.comment=r.comment?`${n.comment}
${r.comment}`:n.comment}n=i}s.items[t]=T(n)?n:new v(n)}}else e("Expected a sequence for this tag");return s}function xt(s,e,t){let{replacer:n}=t,i=new B(s);i.tag="tag:yaml.org,2002:pairs";let r=0;if(e&&Symbol.iterator in Object(e))for(let o of e){typeof n=="function"&&(o=n.call(e,String(r++),o));let a,l;if(Array.isArray(o))if(o.length===2)a=o[0],l=o[1];else throw new TypeError(`Expected [key, value] tuple: ${o}`);else if(o&&o instanceof Object){let c=Object.keys(o);if(c.length===1)a=c[0],l=o[a];else throw new TypeError(`Expected tuple with one key, not ${c.length} keys`)}else a=o;i.items.push(Ae(a,l,t))}return i}var De={collection:"seq",default:!1,tag:"tag:yaml.org,2002:pairs",resolve:Ft,createNode:xt};var Ee=class s extends B{constructor(){super(),this.add=$.prototype.add.bind(this),this.delete=$.prototype.delete.bind(this),this.get=$.prototype.get.bind(this),this.has=$.prototype.has.bind(this),this.set=$.prototype.set.bind(this),this.tag=s.tag}toJSON(e,t){if(!t)return super.toJSON(e);let n=new Map;t?.onCreate&&t.onCreate(n);for(let i of this.items){let r,o;if(T(i)?(r=M(i.key,"",t),o=M(i.value,r,t)):r=M(i,"",t),n.has(r))throw new Error("Ordered maps must not include duplicate keys");n.set(r,o)}return n}static from(e,t,n){let i=xt(e,t,n),r=new this;return r.items=i.items,r}};Ee.tag="tag:yaml.org,2002:omap";var Ke={collection:"seq",identify:s=>s instanceof Map,nodeClass:Ee,default:!1,tag:"tag:yaml.org,2002:omap",resolve(s,e){let t=Ft(s,e),n=[];for(let{key:i}of t.items)E(i)&&(n.includes(i.value)?e(`Ordered maps must not include duplicate keys: ${i.value}`):n.push(i.value));return Object.assign(new Ee,t)},createNode:(s,e,t)=>Ee.from(s,e,t)};function Os({value:s,source:e},t){return e&&(s?Ut:Vt).test.test(e)?e:s?t.options.trueStr:t.options.falseStr}var Ut={identify:s=>s===!0,default:!0,tag:"tag:yaml.org,2002:bool",test:/^(?:Y|y|[Yy]es|YES|[Tt]rue|TRUE|[Oo]n|ON)$/,resolve:()=>new y(!0),stringify:Os},Vt={identify:s=>s===!1,default:!0,tag:"tag:yaml.org,2002:bool",test:/^(?:N|n|[Nn]o|NO|[Ff]alse|FALSE|[Oo]ff|OFF)$/,resolve:()=>new y(!1),stringify:Os};var As={identify:s=>typeof s=="number",default:!0,tag:"tag:yaml.org,2002:float",test:/^(?:[-+]?\.(?:inf|Inf|INF)|\.nan|\.NaN|\.NAN)$/,resolve:s=>s.slice(-3).toLowerCase()==="nan"?NaN:s[0]==="-"?Number.NEGATIVE_INFINITY:Number.POSITIVE_INFINITY,stringify:D},Es={identify:s=>typeof s=="number",default:!0,tag:"tag:yaml.org,2002:float",format:"EXP",test:/^[-+]?(?:[0-9][0-9_]*)?(?:\.[0-9_]*)?[eE][-+]?[0-9]+$/,resolve:s=>parseFloat(s.replace(/_/g,"")),stringify(s){let e=Number(s.value);return isFinite(e)?e.toExponential():D(s)}},Ts={identify:s=>typeof s=="number",default:!0,tag:"tag:yaml.org,2002:float",test:/^[-+]?(?:[0-9][0-9_]*)?\.[0-9_]*$/,resolve(s){let e=new y(parseFloat(s.replace(/_/g,""))),t=s.indexOf(".");if(t!==-1){let n=s.substring(t+1).replace(/_/g,"");n[n.length-1]==="0"&&(e.minFractionDigits=n.length)}return e},stringify:D};var je=s=>typeof s=="bigint"||Number.isInteger(s);function St(s,e,t,{intAsBigInt:n}){let i=s[0];if((i==="-"||i==="+")&&(e+=1),s=s.substring(e).replace(/_/g,""),n){switch(t){case 2:s=`0b${s}`;break;case 8:s=`0o${s}`;break;case 16:s=`0x${s}`;break}let o=BigInt(s);return i==="-"?BigInt(-1)*o:o}let r=parseInt(s,t);return i==="-"?-1*r:r}function Jt(s,e,t){let{value:n}=s;if(je(n)){let i=n.toString(e);return n<0?"-"+t+i.substr(1):t+i}return D(s)}var Is={identify:je,default:!0,tag:"tag:yaml.org,2002:int",format:"BIN",test:/^[-+]?0b[0-1_]+$/,resolve:(s,e,t)=>St(s,2,2,t),stringify:s=>Jt(s,2,"0b")},Ls={identify:je,default:!0,tag:"tag:yaml.org,2002:int",format:"OCT",test:/^[-+]?0[0-7_]+$/,resolve:(s,e,t)=>St(s,1,8,t),stringify:s=>Jt(s,8,"0")},Cs={identify:je,default:!0,tag:"tag:yaml.org,2002:int",test:/^[-+]?[0-9][0-9_]*$/,resolve:(s,e,t)=>St(s,0,10,t),stringify:D},vs={identify:je,default:!0,tag:"tag:yaml.org,2002:int",format:"HEX",test:/^[-+]?0x[0-9a-fA-F_]+$/,resolve:(s,e,t)=>St(s,2,16,t),stringify:s=>Jt(s,16,"0x")};var Te=class s extends ${constructor(e){super(e),this.tag=s.tag}add(e){let t;T(e)?t=e:e&&typeof e=="object"&&"key"in e&&"value"in e&&e.value===null?t=new v(e.key,null):t=new v(e,null),fe(this.items,t.key)||this.items.push(t)}get(e,t){let n=fe(this.items,e);return!t&&T(n)?E(n.key)?n.key.value:n.key:n}set(e,t){if(typeof t!="boolean")throw new Error(`Expected boolean value for set(key, value) in a YAML set, not ${typeof t}`);let n=fe(this.items,e);n&&!t?this.items.splice(this.items.indexOf(n),1):!n&&t&&this.items.push(new v(e))}toJSON(e,t){return super.toJSON(e,t,Set)}toString(e,t,n){if(!e)return JSON.stringify(this);if(this.hasAllNullValues(!0))return super.toString(Object.assign({},e,{allNullValues:!0}),t,n);throw new Error("Set items must all have null values")}static from(e,t,n){let{replacer:i}=n,r=new this(e);if(t&&Symbol.iterator in Object(t))for(let o of t)typeof i=="function"&&(o=i.call(t,o,o)),r.items.push(Ae(o,null,n));return r}};Te.tag="tag:yaml.org,2002:set";var qe={collection:"map",identify:s=>s instanceof Set,nodeClass:Te,default Line truncated
`)?(t.push("..."),t.push(x(l,""))):t.push(`... ${l}`)}else t.push("...");else{let l=s.comment;l&&o&&(l=l.replace(/^\n+/,"")),l&&((!o||a)&&t[t.length-1]!==""&&t.push(""),t.push(x(r(l),"")))}return t.join(`
`)+`
`}var he=class s{constructor(e,t,n){this.commentBefore=null,this.comment=null,this.errors=[],this.warnings=[],Object.defineProperty(this,K,{value:He});let i=null;typeof t=="function"||Array.isArray(t)?i=t:n===void 0&&t&&(n=t,t=void 0);let r=Object.assign({intAsBigInt:!1,keepSourceTokens:!1,logLevel:"warn",prettyErrors:!0,strict:!0,stringKeys:!1,uniqueKeys:!0,version:"1.2"},n);this.options=r;let{version:o}=r;n?._directives?(this.directives=n._directives.atDocument(),this.directives.yaml.explicit&&(o=this.directives.yaml.version)):this.directives=new Z({version:o}),this.setSchema(o,n),this.contents=e===void 0?null:this.createNode(e,i,n)}clone(){let e=Object.create(s.prototype,{[K]:{value:He}});return e.commentBefore=this.commentBefore,e.comment=this.comment,e.errors=this.errors.slice(),e.warnings=this.warnings.slice(),e.options=Object.assign({},this.options),this.directives&&(e.directives=this.directives.clone()),e.schema=this.schema.clone(),e.contents=I(this.contents)?this.contents.clone(e.schema):this.contents,this.range&&(e.range=this.range.slice()),e}add(e){Le(this.contents)&&this.contents.add(e)}addIn(e,t){Le(this.contents)&&this.contents.addIn(e,t)}createAlias(e,t){if(!e.anchor){let n=_t(this);e.anchor=!t||n.has(t)?Pt(t||"a",n):t}return new ie(e.anchor)}createNode(e,t,n){let i;if(typeof t=="function")e=t.call({"":e},"",e),i=t;else if(Array.isArray(t)){let m=S=>typeof S=="number"||S instanceof String||S instanceof Number,w=t.filter(m).map(String);w.length>0&&(t=t.concat(w)),i=t}else n===void 0&&t&&(n=t,t=void 0);let{aliasDuplicateObjects:r,anchorPrefix:o,flow:a,keepUndefined:l,onTagObj:c,tag:p}=n??{},{onAnchor:f,setAnchors:u,sourceObjects:d}=ps(this,o||"a"),g={aliasDuplicateObjects:r??!0,keepUndefined:l??!1,onAnchor:f,onTagObj:c,replacer:i,schema:this.schema,sourceObjects:d},h=re(e,p,g);return a&&L(h)&&(h.flow=!0),u(),h}createPair(e,t,n={}){let i=this.createNode(e,null,n),r=this.createNode(t,null,n);return new v(i,r)}delete(e){return Le(this.contents)?this.contents.delete(e):!1}deleteIn(e){return Ne(e)?this.contents==null?!1:(this.contents=null,!0):Le(this.contents)?this.contents.deleteIn(e):!1}get(e,t){return L(this.contents)?this.contents.get(e,t):void 0}getIn(e,t){return Ne(e)?!t&&E(this.contents)?this.contents.value:this.contents:L(this.contents)?this.contents.getIn(e,t):void 0}has(e){return L(this.contents)?this.contents.has(e):!1}hasIn(e){return Ne(e)?this.contents!==void 0:L(this.contents)?this.contents.hasIn(e):!1}set(e,t){this.contents==null?this.contents=ve(this.schema,[e],t):Le(this.contents)&&this.contents.set(e,t)}setIn(e,t){Ne(e)?this.contents=t:this.contents==null?this.contents=ve(this.schema,Array.from(e),t):Le(this.contents)&&this.contents.setIn(e,t)}setSchema(e,t={}){typeof e=="number"&&(e=String(e));let n;switch(e){case"1.1":this.directives?this.directives.yaml.version="1.1":this.directives=new Z({version:"1.1"}),n={resolveKnownTags:!1,schema:"yaml-1.1"};break;case"1.2":case"next":this.directives?this.directives.yaml.version=e:this.directives=new Z({version:e}),n={resolveKnownTags:!0,schema:"core"};break;case null:this.directives&&delete this.directives,n=null;break;default:{let i=JSON.stringify(e);throw new Error(`Expected '1.1', '1.2' or null as first argument, but found: ${i}`)}}if(t.schema instanceof Object)this.schema=t.schema;else if(n)this.schema=new Re(Object.assign(n,t));else throw new Error("With a null YAML version, the { schema: Schema } option is required")}toJS({json:e,jsonArg:t,mapAsMap:n,maxAliasCount:i,onAnchor:r,reviver:o}={}){let a={anchors:new Map,doc:this,keep:!e,mapAsMap:n===!0,mapKeyWarned:!1,maxAliasCount:typeof i=="number"?i:100},l=M(this.contents,t??"",a);if(typeof r=="function")for(let{count:c,res:p}of a.anchors.values())r(p,c);return typeof o=="function"?le(o,{"":l},"",l):l}toJSON(e,t){return this.toJS({json:!0,jsonArg:e,mapAsMap:!1,onAnchor:t})}toString(e={}){if(this.errors.length>0)throw new Error("Document with errors cannot be stringified");if("indent"in e&&(!Number.isInteger(e.indent)||Number(e.indent)<=0)){let t=JSON.stringify(e.indent);throw new Error(`"indent" option must be a positive integer, not ${t}`)}return Bs(this,e)}};function Le(s){if(L(s))return!0;throw new Error("Expected a YAML collection as document contents")}var Fe=class extends Error{constructor(e,t,n,i){super(),this.name=e,this.code=n,this.message=i,this.pos=t}},G=class extends Fe{constructor(e,t,n){super("YAMLParseError",e,t,n)}},xe=class extends Fe{constructor(e,t,n){super("YAMLWarning",e,t,n)}},Gt=(s,e)=>t=>{if(t.pos[0]===-1)return;t.linePos=t.pos.map(a=>e.linePos(a));let{line:n,col:i}=t.linePos[0];t.message+=` at line ${n}, column ${i}`;let r=i-1,o=s.substring(e.lineStarts[n-1],e.lineStarts[n]).replace(/[\n\r]+$/,"");if(r>=60&&o.length>80){let a=Math.min(r-39,o.length-79);o="\u2026"+o.substring(a),r-=a-1}if(o.length>80&&(o=o.substring(0,79)+"\u2026"),n>1&&/^ *$/.test(o.substring(0,r))){let a=s.substring(e.lineStarts[n-2],e.lineStarts[n-1]);a.length>80&&(a=a.substring(0,79)+`\u202Line truncated
`),o=a+o}if(/[^ ]/.test(o)){let a=1,l=t.linePos[1];l?.line===n&&l.col>i&&(a=Math.max(1,Math.min(l.col-i,80-r)));let c=" ".repeat(r)+"^".repeat(a);t.message+=`:
${o}
${c}
`}};function te(s,{flow:e,indicator:t,next:n,offset:i,onError:r,parentIndent:o,startOnNewline:a}){let l=!1,c=a,p=a,f="",u="",d=!1,g=!1,h=null,m=null,w=null,S=null,k=null,N=null,O=null;for(let b of s)switch(g&&(b.type!=="space"&&b.type!=="newline"&&b.type!=="comma"&&r(b.offset,"MISSING_CHAR","Tags and anchors must be separated from the next token by white space"),g=!1),h&&(c&&b.type!=="comment"&&b.type!=="newline"&&r(h,"TAB_AS_INDENT","Tabs are not allowed as indentation"),h=null),b.type){case"space":!e&&(t!=="doc-start"||n?.type!=="flow-collection")&&b.source.includes(" ")&&(h=b),p=!0;break;case"comment":{p||r(b,"MISSING_CHAR","Comments must be separated from other tokens by white space characters");let _=b.source.substring(1)||" ";f?f+=u+_:f=_,u="",c=!1;break}case"newline":c?f?f+=b.source:(!N||t!=="seq-item-ind")&&(l=!0):u+=b.source,c=!0,d=!0,(m||w)&&(S=b),p=!0;break;case"anchor":m&&r(b,"MULTIPLE_ANCHORS","A node can have at most one anchor"),b.source.endsWith(":")&&r(b.offset+b.source.length-1,"BAD_ALIAS","Anchor ending in : is ambiguous",!0),m=b,O??(O=b.offset),c=!1,p=!1,g=!0;break;case"tag":{w&&r(b,"MULTIPLE_TAGS","A node can have at most one tag"),w=b,O??(O=b.offset),c=!1,p=!1,g=!0;break}case t:(m||w)&&r(b,"BAD_PROP_ORDER",`Anchors and tags must be after the ${b.source} indicator`),N&&r(b,"UNEXPECTED_TOKEN",`Unexpected ${b.source} in ${e??"collection"}`),N=b,c=t==="seq-item-ind"||t==="explicit-key-ind",p=!1;break;case"comma":if(e){k&&r(b,"UNEXPECTED_TOKEN",`Unexpected , in ${e}`),k=b,c=!1,p=!1;break}default:r(b,"UNEXPECTED_TOKEN",`Unexpected ${b.type} token`),c=!1,p=!1}let A=s[s.length-1],C=A?A.offset+A.source.length:i;return g&&n&&n.type!=="space"&&n.type!=="newline"&&n.type!=="comma"&&(n.type!=="scalar"||n.source!=="")&&r(n.offset,"MISSING_CHAR","Tags and anchors must be separated from the next token by white space"),h&&(c&&h.indent<=o||n?.type==="block-map"||n?.type==="block-seq")&&r(h,"TAB_AS_INDENT","Tabs are not allowed as indentation"),{comma:k,found:N,spaceBefore:l,comment:f,hasNewline:d,anchor:m,tag:w,newlineAfterProp:S,end:C,start:O??C}}function pe(s){if(!s)return null;switch(s.type){case"alias":case"scalar":case"double-quoted-scalar":case"single-quoted-scalar":if(s.source.includes(`
`))return!0;if(s.end){for(let e of s.end)if(e.type==="newline")return!0}return!1;case"flow-collection":for(let e of s.items){for(let t of e.start)if(t.type==="newline")return!0;if(e.sep){for(let t of e.sep)if(t.type==="newline")return!0}if(pe(e.key)||pe(e.value))return!0}return!1;default:return!0}}function Ue(s,e,t){if(e?.type==="flow-collection"){let n=e.end[0];n.indent===s&&(n.source==="]"||n.source==="}")&&pe(e)&&t(n,"BAD_INDENT","Flow end indicator should be more indented than parent",!0)}}function At(s,e,t){let{uniqueKeys:n}=s.options;if(n===!1)return!1;let i=typeof n=="function"?n:(r,o)=>r===o||E(r)&&E(o)&&r.value===o.value;return e.some(r=>i(r.key,t))}var Ds="All mapping items must start at the same column";function Ks({composeNode:s,composeEmptyNode:e},t,n,i,r){let o=r?.nodeClass??$,a=new o(t.schema);t.atRoot&&(t.atRoot=!1);let l=n.offset,c=null;for(let p of n.items){let{start:f,key:u,sep:d,value:g}=p,h=te(f,{indicator:"explicit-key-ind",next:u??d?.[0],offset:l,onError:i,parentIndent:n.indent,startOnNewline:!0}),m=!h.found;if(m){if(u&&(u.type==="block-seq"?i(l,"BLOCK_AS_IMPLICIT_KEY","A block sequence may not be used as an implicit map key"):"indent"in u&&u.indent!==n.indent&&i(l,"BAD_INDENT",Ds)),!h.anchor&&!h.tag&&!d){c=h.end,h.comment&&(a.comment?a.comment+=`
`+h.comment:a.comment=h.comment);continue}(h.newlineAfterProp||pe(u))&&i(u??f[f.length-1],"MULTILINE_IMPLICIT_KEY","Implicit keys need to be on a single line")}else h.found?.indent!==n.indent&&i(l,"BAD_INDENT",Ds);t.atKey=!0;let w=h.end,S=u?s(t,u,h,i):e(t,w,f,null,h,i);t.schema.compat&&Ue(n.indent,u,i),t.atKey=!1,At(t,a.items,S)&&i(w,"DUPLICATE_KEY","Map keys must be unique");let k=te(d??[],{indicator:"map-value-ind",next:g,offset:S.range[2],onError:i,parentIndent:n.indent,startOnNewline:!u||u.type==="block-scalar"});if(l=k.end,k.found){m&&(g?.type==="block-map"&&!k.hasNewline&&i(l,"BLOCK_AS_IMPLICIT_KEY","Nested mappings are not allowed in compact mappings"),t.options.strict&&h.start<k.found.offset-1024&&i(S.range,"KEY_OVER_1024_CHARS","The : indicator must be at most 1024 chars after the start of an implicit block mapping key"));let N=g?s(t,g,k,i):e(t,l,d,null,k,i);t.schema.compat&&Ue(n.indent,g,i),l=N.range[2];let O=new v(S,N);t.options.keepSourceTokens&&(O.srcToken=p),a.items.push(O)}else{m&&i(S.range,"MISSING_CHAR","Implicit map keys need to be followed by map values"),k.comment&&(S.comment?S.comment+=`
`+k.comment:S.comment=k.comment);let N=new v(S);t.options.keepSourceTokens&&(N.srcToken=p),a.items.push(N)}}return c&&c<l&&i(c,"IMPOSSIBLE","Map comment with trailing content"),a.range=[n.offset,l,c??l],a}function js({composeNode:s,composeEmptyNode:e},t,n,i,r){let o=r?.nodeClass??B,a=new o(t.schema);t.atRoot&&(t.atRoot=!1),t.atKey&&(t.atKey=!1);let l=n.offset,c=null;for(let{start:p,value:f}of n.items){let u=te(p,{indicator:"seq-item-ind",next:f,offset:l,onError:i,parentIndent:n.indent,startOnNewline:!0});if(!u.found)if(u.anchor||u.tag||f)f?.type==="block-seq"?i(u.end,"BAD_INDENT","All sequence items must start at the same column"):i(l,"MISSING_CHAR","Sequence item without - indicator");else{c=u.end,u.comment&&(a.comment=u.comment);continue}let d=f?s(t,f,u,i):e(t,u.end,p,null,u,i);t.schema.compat&&Ue(n.indent,f,i),l=d.range[2],a.items.push(d)}return a.range=[n.offset,l,c??l],a}function se(s,e,t,n){let i="";if(s){let r=!1,o="";for(let a of s){let{source:l,type:c}=a;switch(c){case"space":r=!0;break;case"comment":{t&&!r&&n(a,"MISSING_CHAR","Comments must be separated from other tokens by white space characters");let p=l.substring(1)||" ";i?i+=o+p:i=p,o="";break}case"newline":i&&(o+=l),r=!0;break;default:n(a,"UNEXPECTED_TOKEN",`Unexpected ${c} at node end`)}e+=l.length}}return{comment:i,offset:e}}var Wt="Block collections are not allowed within flow collections",Ht=s=>s&&(s.type==="block-map"||s.type==="block-seq");function qs({composeNode:s,composeEmptyNode:e},t,n,i,r){let o=n.start.source==="{",a=o?"flow map":"flow sequence",l=r?.nodeClass??(o?$:B),c=new l(t.schema);c.flow=!0;let p=t.atRoot;p&&(t.atRoot=!1),t.atKey&&(t.atKey=!1);let f=n.offset+n.start.source.length;for(let m=0;m<n.items.length;++m){let w=n.items[m],{start:S,key:k,sep:N,value:O}=w,A=te(S,{flow:a,indicator:"explicit-key-ind",next:k??N?.[0],offset:f,onError:i,parentIndent:n.indent,startOnNewline:!1});if(!A.found){if(!A.anchor&&!A.tag&&!N&&!O){m===0&&A.comma?i(A.comma,"UNEXPECTED_TOKEN",`Unexpected , in ${a}`):m<n.items.length-1&&i(A.start,"UNEXPECTED_TOKEN",`Unexpected empty item in ${a}`),A.comment&&(c.comment?c.comment+=`
`+A.comment:c.comment=A.comment),f=A.end;continue}!o&&t.options.strict&&pe(k)&&i(k,"MULTILINE_IMPLICIT_KEY","Implicit keys of flow sequence pairs need to be on a single line")}if(m===0)A.comma&&i(A.comma,"UNEXPECTED_TOKEN",`Unexpected , in ${a}`);else if(A.comma||i(A.start,"MISSING_CHAR",`Missing , between ${a} items`),A.comment){let C="";e:for(let b of S)switch(b.type){case"comma":case"space":break;case"comment":C=b.source.substring(1);break e;default:break e}if(C){let b=c.items[c.items.length-1];T(b)&&(b=b.value??b.key),b.comment?b.comment+=`
`+C:b.comment=C,A.comment=A.comment.substring(C.length+1)}}if(!o&&!N&&!A.found){let C=O?s(t,O,A,i):e(t,A.end,N,null,A,i);c.items.push(C),f=C.range[2],Ht(O)&&i(C.range,"BLOCK_IN_FLOW",Wt)}else{t.atKey=!0;let C=A.end,b=k?s(t,k,A,i):e(t,C,S,null,A,i);Ht(k)&&i(b.range,"BLOCK_IN_FLOW",Wt),t.atKey=!1;let _=te(N??[],{flow:a,indicator:"map-value-ind",next:O,offset:b.range[2],onError:i,parentIndent:n.indent,startOnNewline:!1});if(_.found){if(!o&&!A.found&&t.options.strict){if(N)for(let P of N){if(P===_.found)break;if(P.type==="newline"){i(P,"MULTILINE_IMPLICIT_KEY","Implicit keys of flow sequence pairs need to be on a single line");break}}A.start<_.found.offset-1024&&i(_.found,"KEY_OVER_1024_CHARS","The : indicator must be at most 1024 chars after the start of an implicit flow sequence key")}}else O&&("source"in O&&O.source?.[0]===":"?i(O,"MISSING_CHAR",`Missing space after : in ${a}`):i(_.start,"MISSING_CHAR",`Missing , or : between ${a} items`));let ae=O?s(t,O,_,i):_.found?e(t,_.end,N,null,_,i):null;ae?Ht(O)&&i(ae.range,"BLOCK_IN_FLOW",Wt):_.comment&&(b.comment?b.comment+=`
`+_.comment:b.comment=_.comment);let be=new v(b,ae);if(t.options.keepSourceTokens&&(be.srcToken=w),o){let P=c;At(t,P.items,b)&&i(C,"DUPLICATE_KEY","Map keys must be unique"),P.items.push(be)}else{let P=new $(t.schema);P.flow=!0,P.items.push(be);let ls=(ae??b).range;P.range=[b.range[0],ls[1],ls[2]],c.items.push(P)}f=ae?ae.range[2]:_.end}}let u=o?"}":"]",[d,...g]=n.end,h=f;if(d?.source===u)h=d.offset+d.source.length;else{let m=a[0].toUpperCase()+a.substring(1),w=p?`${m} must end with a ${u}`:`${m} in block collection must be sufficiently indented and end with a ${u}`;i(f,p?"MISSING_CHAR":"BAD_INDENT",w),d&&d.source.length!==1&&g.unshift(d)}if(g.length>0){let m=se(g,h,t.options.strict,i);m.comment&&(c.comment?c.comment+=`
`+m.comment:c.comment=m.comment),c.range=[n.offset,h,m.offset]}else c.range=[n.offset,h,h];return c}function Xt(s,e,t,n,i,r){let o=t.type==="block-map"?Ks(s,e,t,n,r):t.type==="block-seq"?js(s,e,t,n,r):qs(s,e,t,n,r),a=o.constructor;return i==="!"||i===a.tagName?(o.tag=a.tagName,o):(i&&(o.tag=i),o)}function Rs(s,e,t,n,i){let r=n.tag,o=r?e.directives.tagName(r.source,u=>i(r,"TAG_RESOLVE_FAILED",u)):null;if(t.type==="block-seq"){let{anchor:u,newlineAfterProp:d}=n,g=u&&r?u.offset>r.offset?u:r:u??r;g&&(!d||d.offset<g.offset)&&i(g,"MISSING_CHAR","Missing newline after block sequence props")}let a=t.type==="block-map"?"map":t.type==="block-seq"?"seq":t.start.source==="{"?"map":"seq";if(!r||!o||o==="!"||o===$.tagName&&a==="map"||o===B.tagName&&a==="seq")return Xt(s,e,t,i,o);let l=e.schema.tags.find(u=>u.tag===o&&u.collection===a);if(!l){let u=e.schema.knownTags[o];if(u?.collection===a)e.schema.tags.push(Object.assign({},u,{default:!1})),l=u;else return u?i(r,"BAD_COLLECTION_TYPE",`${u.tag} used for ${a} collection, but expects ${u.collection??"scalar"}`,!0):i(r,"TAG_RESOLVE_FAILED",`Unresolved tag: ${o}`,!0),Xt(s,e,t,i,o)}let c=Xt(s,e,t,i,o,l),p=l.resolve?.(c,u=>i(r,"TAG_RESOLVE_FAILED",u),e.options)??c,f=I(p)?p:new y(p);return f.range=c.range,f.tag=o,l?.format&&(f.format=l.format),f}function zt(s,e,t){let n=e.offset,i=wn(e,s.options.strict,t);if(!i)return{value:"",type:null,comment:"",range:[n,n,n]};let r=i.mode===">"?y.BLOCK_FOLDED:y.BLOCK_LITERAL,o=e.source?Sn(e.source):[],a=o.length;for(let h=o.length-1;h>=0;--h){let m=o[h][1];if(m===""||m==="\r")a=h;else break}if(a===0){let h=i.chomp==="+"&&o.length>0?`
`.repeat(Math.max(1,o.length-1)):"",m=n+i.length;return e.source&&(m+=e.source.length),{value:h,type:r,comment:i.comment,range:[n,m,m]}}let l=e.indent+i.indent,c=e.offset+i.length,p=0;for(let h=0;h<a;++h){let[m,w]=o[h];if(w===""||w==="\r")i.indent===0&&m.length>l&&(l=m.length);else{m.length<l&&t(c+m.length,"MISSING_CHAR","Block scalars with more-indented leading empty lines must use an explicit indentation indicator"),i.indent===0&&(l=m.length),p=h,l===0&&!s.atRoot&&t(c,"BAD_INDENT","Block scalar values in collections must be indented");break}c+=m.length+w.length+1}for(let h=o.length-1;h>=a;--h)o[h][0].length>l&&(a=h+1);let f="",u="",d=!1;for(let h=0;h<p;++h)f+=o[h][0].slice(l)+`
`;for(let h=p;h<a;++h){let[m,w]=o[h];c+=m.length+w.length+1;let S=w[w.length-1]==="\r";if(S&&(w=w.slice(0,-1)),w&&m.length<l){let N=`Block scalar lines must not be less indented than their ${i.indent?"explicit indentation indicator":"first line"}`;t(c-w.length-(S?2:1),"BAD_INDENT",N),m=""}r===y.BLOCK_LITERAL?(f+=u+m.slice(l)+w,u=`
`):m.length>l||w[0]===" "?(u===" "?u=`
`:!d&&u===`
`&&(u=`
`),f+=u+m.slice(l)+w,u=`
`,d=!0):w===""?u===`
`?f+=`
`:u=`
`:(f+=u+w,u=" ",d=!1)}switch(i.chomp){case"-":break;case"+":for(let h=a;h<o.length;++h)f+=`
`+o[h][0].slice(l);f[f.length-1]!==`
`&&(f+=`
`);break;default:f+=`
`}let g=n+i.length+e.source.length;return{value:f,type:r,comment:i.comment,range:[n,g,g]}}function wn({offset:s,props:e},t,n){if(e[0].type!=="block-scalar-header")return n(e[0],"IMPOSSIBLE","Block scalar header not found"),null;let{source:i}=e[0],r=i[0],o=0,a="",l=-1;for(let u=1;u<i.length;++u){let d=i[u];if(!a&&(d==="-"||d==="+"))a=d;else{let g=Number(d);!o&&g?o=g:l===-1&&(l=s+u)}}l!==-1&&n(l,"UNEXPECTED_TOKEN",`Block scalar header includes extra characters: ${i}`);let c=!1,p="",f=i.length;for(let u=1;u<e.length;++u){let d=e[u];switch(d.type){case"space":c=!0;case"newline":f+=d.source.length;break;case"comment":t&&!c&&n(d,"MISSING_CHAR","Comments must be separated from other tokens by white space characters"),f+=d.source.length,p=d.source.substring(1);break;case"error":n(d,"UNEXPECTED_TOKEN",d.message),f+=d.source.length;break;default:{let g=`Unexpected token in block scalar header: ${d.type}`;n(d,"UNEXPECTED_TOKEN",g);let h=d.source;h&&typeof h=="string"&&(f+=h.length)}}}return{mode:r,indent:o,chomp:a,comment:p,length:f}}function Sn(s){let e=s.split(/\n( *)/),t=e[0],n=t.match(/^( *)/),r=[n?.[1]?[n[1],t.slice(n[1].length)]:["",t]];for(let o=1;o<e.length;o+=2)r.push([e[o],e[o+1]]);return r}function Zt(s,e,t){let{offset:n,type:i,source:r,end:o}=s,a,l,c=(u,d,g)=>t(n+u,d,g);switch(i){case"scalar":a=y.PLAIN,l=kn(r,c);break;case"single-quoted-scalar":a=y.QUOTE_SINGLE,l=Nn(r,c);break;case"double-quoted-scalar":a=y.QUOTE_DOUBLE,l=On(r,c);break;default:return t(s,"UNEXPECTED_TOKEN",`Expected a flow scalar value, but found: ${i}`),{value:"",type:null,comment:"",range:[n,n+r.length,n+r.length]}}let p=n+r.length,f=se(o,p,e,t);return{value:l,type:a,comment:f.comment,range:[n,p,f.offset]}}function kn(s,e){let t="";switch(s[0]){case" ":t="a tab character";break;case",":t="flow indicator character ,";break;case"%":t="directive indicator character %";break;case"|":case">":{t=`block scalar indicator ${s[0]}`;break}case"@":case"`":{t=`reserved character ${s[0]}`;break}}return t&&e(0,"BAD_SCALAR_START",`Plain value cannot start with ${t}`),Fs(s)}function Nn(s,e){return(s[s.length-1]!=="'"||s.length===1)&&e(s.length,"MISSING_CHAR","Missing closing 'quote"),Fs(s.slice(1,-1)).replace(/''/g,"'")}function Fs(s){let e=/(.*?)\r?\n/sy,t=e.exec(s);if(!t)return s;let n,i;try{n=new RegExp("(?<![ ])[ ]+$"),i=new RegExp("^[ ]+|(?<![ ])[ ]+$","g")}catch{n=/[ \t]+$/,i=/^[ \t]+|[ \t]+$/g}let r=t[1].replace(n,""),o=" ",a=e.lastIndex;for(;t=e.exec(s);){let c=t[1].replace(i,"");c===""?o===`
`?r+=o:o=`
`:(r+=o+c,o=" "),a=e.lastIndex}let l=/[ \t]*(.*)/sy;return l.lastIndex=a,t=l.exec(s),r+o+(t?.[1]??"")}function On(s,e){let t="";for(let n=1;n<s.length-1;++n){let i=s[n];if(!(i==="\r"&&s[n+1]===`
`))if(i===`
`){let{fold:r,offset:o}=An(s,n);t+=r,n=o}else if(i==="\\"){let r=s[++n],o=En[r];if(o)t+=o;else if(r===`
`)for(r=s[n+1];r===" "||r===" ";)r=s[++n+1];else if(r==="\r"&&s[n+1]===`
`)for(r=s[++n+1];r===" "||r===" ";)r=s[++n+1];else if(r==="x"||r==="u"||r==="U"){let a=r==="x"?2:r==="u"?4:8;t+=Tn(s,n+1,a,e),n+=a}else{let a=s.substr(n-1,2);e(n-1,"BAD_DQ_ESCAPE",`Invalid escape sequence ${a}`),t+=a}}else if(i===" "||i===" "){let r=n,o=s[n+1];for(;o===" "||o===" ";)o=s[++n+1];o!==`
`&&!(o==="\r"&&s[n+2]===`
`)&&(t+=n>r?s.slice(r,n+1):i)}else t+=i}return(s[s.length-1]!=='"'||s.length===1)&&e(s.length,"MISSING_CHAR",'Missing closing "quote'),t}function An(s,e){let t="",n=s[e+1];for(;(n===" "||n===" "||n===`
`||n==="\r")&&!(n==="\r"&&s[e+2]!==`
`);)n===`
`&&(t+=`
`),e+=1,n=s[e+1];return t||(t=" "),{fold:t,offset:e}}var En={0:"\0",a:"\x07",b:"\b",e:"\x1B",f:"\f",n:`
`,r:"\r",t:" ",v:"\v",N:"\x85",_:"\xA0",L:"\u2028",P:"\u2029"," ":" ",'"':'"',"/":"/","\\":"\\"," ":" "};function Tn(s,e,t,n){let i=s.substr(e,t),o=i.length===t&&/^[0-9a-fA-F]+$/.test(i)?parseInt(i,16):NaN;try{return String.fromCodePoint(o)}catch{let a=s.substr(e-2,t+2);return n(e-2,"BAD_DQ_ESCAPE",`Invalid escape sequence ${a}`),a}}function es(s,e,t,n){let{value:i,type:r,comment:o,range:a}=e.type==="block-scalar"?zt(s,e,n):Zt(e,s.options.strict,n),l=t?s.directives.tagName(t.source,f=>n(t,"TAG_RESOLVE_FAILED",f)):null,c;s.options.stringKeys&&s.atKey?c=s.schema[q]:l?c=In(s.schema,i,l,t,n):e.type==="scalar"?c=Ln(s,i,e,n):c=s.schema[q];let p;try{let f=c.resolve(i,u=>n(t??e,"TAG_RESOLVE_FAILED",u),s.options);p=E(f)?f:new y(f)}catch(f){let u=f instanceof Error?f.message:String(f);n(t??e,"TAG_RESOLVE_FAILED",u),p=new y(i)}return p.range=a,p.source=i,r&&(p.type=r),l&&(p.tag=l),c.format&&(p.format=c.format),o&&(p.comment=o),p}function In(s,e,t,n,i){if(t==="!")return s[q];let r=[];for(let a of s.tags)if(!a.collection&&a.tag===t)if(a.default&&a.test)r.push(a);else return a;for(let a of r)if(a.test?.test(e))return a;let o=s.knownTags[t];return o&&!o.collection?(s.tags.push(Object.assign({},o,{default:!1,test:void 0})),o):(i(n,"TAG_RESOLVE_FAILED",`Unresolved tag: ${t}`,t!=="tag:yaml.org,2002:str"),s[q])}function Ln({atKey:s,directives:e,schema:t},n,i,r){let o=t.tags.find(a=>(a.default===!0||s&&a.default==="key")&&a.test?.test(n))||t[q];if(t.compat){let a=t.compat.find(l=>l.default&&l.test?.test(n))??t[q];if(o.tag!==a.tag){let l=e.tagString(o.tag),c=e.tagString(a.tag),p=`Value may be parsed as either ${l} or ${c}`;r(i,"TAG_RESOLVE_FAILED",p,!0)}}return o}function xs(s,e,t){if(e){t??(t=e.length);for(let n=t-1;n>=0;--n){let i=e[n];switch(i.type){case"space":case"comment":case"newline":s-=i.source.length;continue}for(i=e[++n];i?.type==="space";)s+=i.source.length,i=e[++n];break}}return s}var Cn={composeNode:ts,composeEmptyNode:Et};function ts(s,e,t,n){let i=s.atKey,{spaceBefore:r,comment:o,anchor:a,tag:l}=t,c,p=!0;switch(e.type){case"alias":c=vn(s,e,n),(a||l)&&n(e,"ALIAS_PROPS","An alias node must not specify any properties");break;case"scalar":case"single-quoted-scalar":case"double-quoted-scalar":case"block-scalar":c=es(s,e,l,n),a&&(c.anchor=a.source.substring(1));break;case"block-map":case"block-seq":case"flow-collection":try{c=Rs(Cn,s,e,t,n),a&&(c.anchor=a.source.substring(1))}catch(f){let u=f instanceof Error?f.message:String(f);n(e,"RESOURCE_EXHAUSTION",u)}break;default:{let f=e.type==="error"?e.message:`Unsupported token (type: ${e.type})`;n(e,"UNEXPECTED_TOKEN",f),p=!1}}return c??(c=Et(s,e.offset,void 0,null,t,n)),a&&c.anchor===""&&n(a,"BAD_ALIAS","Anchor cannot be an empty string"),i&&s.options.stringKeys&&(!E(c)||typeof c.value!="string"||c.tag&&c.tag!=="tag:yaml.org,2002:str")&&n(l??e,"NON_STRING_KEY","With stringKeys, all keys must be strings"),r&&(c.spaceBefore=!0),o&&(e.type==="scalar"&&e.source===""?c.comment=o:c.commentBefore=o),s.options.keepSourceTokens&&p&&(c.srcToken=e),c}function Et(s,e,t,n,{spaceBefore:i,comment:r,anchor:o,tag:a,end:l},c){let p={type:"scalar",offset:xs(e,t,n),indent:-1,source:""},f=es(s,p,a,c);return o&&(f.anchor=o.source.substring(1),f.anchor===""&&c(o,"BAD_ALIAS","Anchor cannot be an empty string")),i&&(f.spaceBefore=!0),r&&(f.comment=r,f.range[2]=l),f}function vn({options:s},{offset:e,source:t,end:n},i){let r=new ie(t.substring(1));r.source===""&&i(e,"BAD_ALIAS","Alias cannot be an empty string"),r.source.endsWith(":")&&i(e+t.length-1,"BAD_ALIAS","Alias ending in : is ambiguous",!0);let o=e+t.length,a=se(n,o,s.strict,i);return r.range=[e,o,a.offset],a.comment&&(r.comment=a.comment),r}function Us(s,e,{offset:t,start:n,value:i,end:r},o){let a=Object.assign({_directives:e},s),l=new he(void 0,a),c={atKey:!1,atRoot:!0,directives:l.directives,options:l.options,schema:l.schema},p=te(n,{indicator:"doc-start",next:i??r?.[0],offset:t,onError:o,parentIndent:0,startOnNewline:!0});p.found&&(l.directives.docStart=!0,i&&(i.type==="block-map"||i.type==="block-seq")&&!p.hasNewline&&o(p.end,"MISSING_CHAR","Block collection cannot start on same line with directives-end marker")),l.contents=i?ts(c,i,p,o):Et(c,p.end,n,null,p,o);let f=l.contents.range[2],u=se(r,f,!1,o);return u.comment&&(l.comment=u.comment),l.range=[t,f,u.offset],l}function Ve(s){if(typeof s=="number")return[s,s+1];if(Array.isArray(s))return s.length===2?s:[s[0],s[1]];let{offset:e,source:t}=s;return[e,e+(typeof t=="string"?t.length:1)]}function Vs(s){let e="",t=!1,n=!1;for(let i=0;i<s.length;++i){let r=s[i];switch(r[0]){case"#":e+=(e===""?"":n?`
`:`
`)+(r.substring(1)||" "),t=!0,n=!1;break;case"%":s[i+1]?.[0]!=="#"&&(i+=1),t=!1;break;default:t||(n=!0),t=!1}}return{comment:e,afterEmptyLine:n}}var Je=class{constructor(e={}){this.doc=null,this.atDirectives=!1,this.prelude=[],this.errors=[],this.warnings=[],this.onError=(t,n,i,r)=>{let o=Ve(t);r?this.warnings.push(new xe(o,n,i)):this.errors.push(new G(o,n,i))},this.directives=new Z({version:e.version||"1.2"}),this.options=e}decorate(e,t){let{comment:n,afterEmptyLine:i}=Vs(this.prelude);if(n){let r=e.contents;if(t)e.comment=e.comment?`${e.comment}
${n}`:n;else if(i||e.directives.docStart||!r)e.commentBefore=n;else if(L(r)&&!r.flow&&r.items.length>0){let o=r.items[0];T(o)&&(o=o.key);let a=o.commentBefore;o.commentBefore=a?`${n}
${a}`:n}else{let o=r.commentBefore;r.commentBefore=o?`${n}
${o}`:n}}if(t){for(let r=0;r<this.errors.length;++r)e.errors.push(this.errors[r]);for(let r=0;r<this.warnings.length;++r)e.warnings.push(this.warnings[r])}else e.errors=this.errors,e.warnings=this.warnings;this.prelude=[],this.errors=[],this.warnings=[]}streamInfo(){return{comment:Vs(this.prelude).comment,directives:this.directives,errors:this.errors,warnings:this.warnings}}*compose(e,t=!1,n=-1){for(let i of e)yield*this.next(i);yield*this.end(t,n)}*next(e){switch(e.type){case"directive":this.directives.add(e.source,(t,n,i)=>{let r=Ve(e);r[0]+=t,this.onError(r,"BAD_DIRECTIVE",n,i)}),this.prelude.push(e.source),this.atDirectives=!0;break;case"document":{let t=Us(this.options,this.directives,e,this.onError);this.atDirectives&&!t.directives.docStart&&this.onError(e,"MISSING_CHAR","Missing directives-end/doc-start indicator line"),this.decorate(t,!1),this.doc&&(yield this.doc),this.doc=t,this.atDirectives=!1;break}case"byte-order-mark":case"space":break;case"comment":case"newline":this.prelude.push(e.source);break;case"error":{let t=e.source?`${e.message}: ${JSON.stringify(e.source)}`:e.message,n=new G(Ve(e),"UNEXPECTED_TOKEN",t);this.atDirectives||!this.doc?this.errors.push(n):this.doc.errors.push(n);break}case"doc-end":{if(!this.doc){let n="Unexpected doc-end without preceding document";this.errors.push(new G(Ve(e),"UNEXPECTED_TOKEN",n));break}this.doc.directives.docEnd=!0;let t=se(e.end,e.offset+e.source.length,this.doc.options.strict,this.onError);if(this.decorate(this.doc,!0),t.comment){let n=this.doc.comment;this.doc.comment=n?`${n}
${t.comment}`:t.comment}this.doc.range[2]=t.offset;break}default:this.errors.push(new G(Ve(e),"UNEXPECTED_TOKEN",`Unsupported token ${e.type}`))}}*end(e=!1,t=-1){if(this.doc)this.decorate(this.doc,!0),yield this.doc,this.doc=null;else if(e){let n=Object.assign({_directives:this.directives},this.options),i=new he(void 0,n);this.atDirectives&&this.onError(t,"MISSING_CHAR","Missing directives-end indicator line"),i.range=[0,t,t],this.decorate(i,!1),yield i}}};var ss=Symbol("break visit"),$n=Symbol("skip children"),Js=Symbol("remove item");function ye(s,e){"type"in s&&s.type==="document"&&(s={start:s.start,value:s.value}),Ys(Object.freeze([]),s,e)}ye.BREAK=ss;ye.SKIP=$n;ye.REMOVE=Js;ye.itemAtPath=(s,e)=>{let t=s;for(let[n,i]of e){let r=t?.[n];if(r&&"items"in r)t=r.items[i];else return}return t};ye.parentCollection=(s,e)=>{let t=ye.itemAtPath(s,e.slice(0,-1)),n=e[e.length-1][0],i=t?.[n];if(i&&"items"in i)return i;throw new Error("Parent collection not found")};function Ys(s,e,t){let n=t(e,s);if(typeof n=="symbol")return n;for(let i of["key","value"]){let r=e[i];if(r&&"items"in r){for(let o=0;o<r.items.length;++o){let a=Ys(Object.freeze(s.concat([[i,o]])),r.items[o],t);if(typeof a=="number")o=a-1;else{if(a===ss)return ss;a===Js&&(r.items.splice(o,1),o-=1)}}typeof n=="function"&&i==="key"&&(n=n(e,s))}}return typeof n=="function"?n(e,s):n}var ns="\uFEFF",is="",rs="",Tt="";function Qs(s){switch(s){case ns:return"byte-order-mark";case is:return"doc-mode";case rs:return"flow-error-end";case Tt:return"scalar";case"---":return"doc-start";case"...":return"doc-end";case"":case`
`:case`\r
`:return"newline";case"-":return"seq-item-ind";case"?":return"explicit-key-ind";case":":return"map-value-ind";case"{":return"flow-map-start";case"}":return"flow-map-end";case"[":return"flow-seq-start";case"]":return"flow-seq-end";case",":return"comma"}switch(s[0]){case" ":case" ":return"space";case"#":return"comment";case"%":return"directive-line";case"*":return"alias";case"&":return"anchor";case"!":return"tag";case"'":return"single-quoted-scalar";case'"':return"double-quoted-scalar";case"|":case">":return"block-scalar-header"}return null}function W(s){switch(s){case void 0:case" ":case`
`:case"\r":case" ":return!0;default:return!1}}var Gs=new Set("0123456789ABCDEFabcdef"),Pn=new Set("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-#;/?:@&=+$_.!~*'()"),It=new Set(",[]{}"),Mn=new Set(` ,[]{}
\r `),os=s=>!s||Mn.has(s),Ye=class{constructor(){this.atEnd=!1,this.blockScalarIndent=-1,this.blockScalarKeep=!1,this.buffer="",this.flowKey=!1,this.flowLevel=0,this.indentNext=0,this.indentValue=0,this.lineEndPos=null,this.next=null,this.pos=0}*lex(e,t=!1){if(e){if(typeof e!="string")throw TypeError("source is not a string");this.buffer=this.buffer?this.buffer+e:e,this.lineEndPos=null}this.atEnd=!t;let n=this.next??"stream";for(;n&&(t||this.hasChars(1));)n=yield*this.parseNext(n)}atLineEnd(){let e=this.pos,t=this.buffer[e];for(;t===" "||t===" ";)t=this.buffer[++e];return!t||t==="#"||t===`
`?!0:t==="\r"?this.buffer[e+1]===`
`:!1}charAt(e){return this.buffer[this.pos+e]}continueScalar(e){let t=this.buffer[e];if(this.indentNext>0){let n=0;for(;t===" ";)t=this.buffer[++n+e];if(t==="\r"){let i=this.buffer[n+e+1];if(i===`
`||!i&&!this.atEnd)return e+n+1}return t===`
`||n>=this.indentNext||!t&&!this.atEnd?e+n:-1}if(t==="-"||t==="."){let n=this.buffer.substr(e,3);if((n==="---"||n==="...")&&W(this.buffer[e+3]))return-1}return e}getLine(){let e=this.lineEndPos;return(typeof e!="number"||e!==-1&&e<this.pos)&&(e=this.buffer.indexOf(`
`,this.pos),this.lineEndPos=e),e===-1?this.atEnd?this.buffer.substring(this.pos):null:(this.buffer[e-1]==="\r"&&(e-=1),this.buffer.substring(this.pos,e))}hasChars(e){return this.pos+e<=this.buffer.length}setNext(e){return this.buffer=this.buffer.substring(this.pos),this.pos=0,this.lineEndPos=null,this.next=e,null}peek(e){return this.buffer.substr(this.pos,e)}*parseNext(e){switch(e){case"stream":return yield*this.parseStream();case"line-start":return yield*this.parseLineStart();case"block-start":return yield*this.parseBlockStart();case"doc":return yield*this.parseDocument();case"flow":return yield*this.parseFlowCollection();case"quoted-scalar":return yield*this.parseQuotedScalar();case"block-scalar":return yield*this.parseBlockScalar();case"plain-scalar":return yield*this.parsePlainScalar()}}*parseStream(){let e=this.getLine();if(e===null)return this.setNext("stream");if(e[0]===ns&&(yield*this.pushCount(1),e=e.substring(1)),e[0]==="%"){let t=e.length,n=e.indexOf("#");for(;n!==-1;){let r=e[n-1];if(r===" "||r===" "){t=n-1;break}else n=e.indexOf("#",n+1)}for(;;){let r=e[t-1];if(r===" "||r===" ")t-=1;else break}let i=(yield*this.pushCount(t))+(yield*this.pushSpaces(!0));return yield*this.pushCount(e.length-i),this.pushNewline(),"stream"}if(this.atLineEnd()){let t=yield*this.pushSpaces(!0);return yield*this.pushCount(e.length-t),yield*this.pushNewline(),"stream"}return yield is,yield*this.parseLineStart()}*parseLineStart(){let e=this.charAt(0);if(!e&&!this.atEnd)return this.setNext("line-start");if(e==="-"||e==="."){if(!this.atEnd&&!this.hasChars(4))return this.setNext("line-start");let t=this.peek(3);if((t==="---"||t==="...")&&W(this.charAt(3)))return yield*this.pushCount(3),this.indentValue=0,this.indentNext=0,t==="---"?"doc":"stream"}return this.indentValue=yield*this.pushSpaces(!1),this.indentNext>this.indentValue&&!W(this.charAt(1))&&(this.indentNext=this.indentValue),yield*this.parseBlockStart()}*parseBlockStart(){let[e,t]=this.peek(2);if(!t&&!this.atEnd)return this.setNext("block-start");if((e==="-"||e==="?"||e===":")&&W(t)){let n=(yield*this.pushCount(1))+(yield*this.pushSpaces(!0));return this.indentNext=this.indentValue+1,this.indentValue+=n,"block-start"}return"doc"}*parseDocument(){yield*this.pushSpaces(!0);let e=this.getLine();if(e===null)return this.setNext("doc");let t=yield*this.pushIndicators();switch(e[t]){case"#":yield*this.pushCount(e.length-t);case void 0:return yield*this.pushNewline(),yield*this.parseLineStart();case"{":case"[":return yield*this.pushCount(1),this.flowKey=!1,this.flowLevel=1,"flow";case"}":case"]":return yield*this.pushCount(1),"doc";case"*":return yield*this.pushUntil(os),"doc";case'"':case"'":return yield*this.parseQuotedScalar();case"|":case">":return t+=yield*this.parseBlockScalarHeader(),t+=yield*this.pushSpaces(!0),yield*this.pushCount(e.length-t),yield*this.pushNewline(),yield*this.parseBlockScalar();default:return yield*this.parsePlainScalar()}}*parseFlowCollection(){let e,t,n=-1;do e=yield*this.pushNewline(),e>0?(t=yield*this.pushSpaces(!1),this.indentValue=n=t):t=0,t+=yield*this.pushSpaces(!0);while(e+t>0);let i=this.getLine();if(i===null)return this.setNext("flow");if((n!==-1&&n<this.indentNext&&i[0]!=="#"||n===0&&(i.startsWith("---")||i.startsWith("..."))&&W(i[3]))&&!(n===this.indentNext-1&&this.flowLevel===1&&(i[0]==="]"||i[0]==="}")))return this.flowLevel=0,yield rs,yield*this.parseLineStart();let r=0;for(;i[r]===",";)r+=yield*this.pushCount(1),r+=yield*this.pushSpaces(!0),this.flowKey=!1;switch(r+=yield*this.pushIndicators(),i[r]){case void 0:return"flow";case"#":return yield*this.pushCount(i.length-r),"flow";case"{":case"[":return yield*this.pushCount(1),this.flowKey=!1,this.flowLevel+=1,"flow";case"}":case"]":return yield*this.pushCount(1),this.flowKey=!0,this.flowLevel-=1,this.flowLevel?"flow":"doc";case"*":return yield*this.pushUntil(os),"flow";case'"':case"'":return this.flowKey=!0,yield*this.parseQuotedScalar();case":":{let o=this.charAt(1);if(this.flowKey||W(o)||o===",")return this.flowKey=!1,yield*this.pushCount(1),yield*this.pushSpaces(!0),"flow"}default:return this.flowKey=!1,yield*this.parsePlainScalar()}}*parseQuotedScalar(){let e=this.charAt(0),t=this.buffer.indexOf(e,this.pos+1);if(e==="'")for(;t!==-1&&this.buffer[t+1]==="'";)t=this.buffer.indexOf("'",t+2);else for(;t!==-1;){let r=0;for(;this.buffer[t-1-r]==="\\";)r+=1;if(r%2===0)break;t=this.buffer.indexOf('"',t+1)}let n=this.buffer.substring(0,t),i=n.indexOf(`
`,this.pos);if(i!==-1){for(;i!==-1;){let r=this.continueScalar(i+1);if(r===-1)break;i=n.indexOf(`
`,r)}i!==-1&&(t=i-(n[i-1]==="\r"?2:1))}if(t===-1){if(!this.atEnd)return this.setNext("quoted-scalar");t=this.buffer.length}return yield*this.pushToIndex(t+1,!1),this.flowLevel?"flow":"doc"}*parseBlockScalarHeader(){this.blockScalarIndent=-1,this.blockScalarKeep=!1;let e=this.pos;for(;;){let t=this.buffer[++e];if(t==="+")this.blockScalarKeep=!0;else if(t>"0"&&t<="9")this.blockScalarIndent=Number(t)-1;else if(t!=="-")break}return yield*this.pushUntil(t=>W(t)||t==="#")}*parseBlockScalar(){let e=this.pos-1,t=0,n;e:for(let r=this.pos;n=this.buffer[r];++r)switch(n){case" ":t+=1;break;case`
`:e=r,t=0;break;case"\r":{let o=this.buffer[r+1];if(!o&&!this.atEnd)return this.setNext("block-scalar");if(o===`
`)break}default:break e}if(!n&&!this.atEnd)return this.setNext("block-scalar");if(t>=this.indentNext){this.blockScalarIndent===-1?this.indentNext=t:this.indentNext=this.blockScalarIndent+(this.indentNext===0?1:this.indentNext);do{let r=this.continueScalar(e+1);if(r===-1)break;e=this.buffer.indexOf(`
`,r)}while(e!==-1);if(e===-1){if(!this.atEnd)return this.setNext("block-scalar");e=this.buffer.length}}let i=e+1;for(n=this.buffer[i];n===" ";)n=this.buffer[++i];if(n===" "){for(;n===" "||n===" "||n==="\r"||n===`
`;)n=this.buffer[++i];e=i-1}else if(!this.blockScalarKeep)do{let r=e-1,o=this.buffer[r];o==="\r"&&(o=this.buffer[--r]);let a=r;for(;o===" ";)o=this.buffer[--r];if(o===`
`&&r>=this.pos&&r+1+t>a)e=r;else break}while(!0);return yield Tt,yield*this.pushToIndex(e+1,!0),yield*this.parseLineStart()}*parsePlainScalar(){let e=this.flowLevel>0,t=this.pos-1,n=this.pos-1,i;for(;i=this.buffer[++n];)if(i===":"){let r=this.buffer[n+1];if(W(r)||e&&It.has(r))break;t=n}else if(W(i)){let r=this.buffer[n+1];if(i==="\r"&&(r===`
`?(n+=1,i=`
`,r=this.buffer[n+1]):t=n),r==="#"||e&&It.has(r))break;if(i===`
`){let o=this.continueScalar(n+1);if(o===-1)break;n=Math.max(n,o-2)}}else{if(e&&It.has(i))break;t=n}return!i&&!this.atEnd?this.setNext("plain-scalar"):(yield Tt,yield*this.pushToIndex(t+1,!0),e?"flow":"doc")}*pushCount(e){return e>0?(yield this.buffer.substr(this.pos,e),this.pos+=e,e):0}*pushToIndex(e,t){let n=this.buffer.slice(this.pos,e);return n?(yield n,this.pos+=n.length,n.length):(t&&(yield""),0)}*pushIndicators(){let e=0;e:for(;;){switch(this.charAt(0)){case"!":e+=yield*this.pushTag(),e+=yield*this.pushSpaces(!0);continue e;case"&":e+=yield*this.pushUntil(os),e+=yield*this.pushSpaces(!0);continue e;case"-":case"?":case":":{let t=this.flowLevel>0,n=this.charAt(1);if(W(n)||t&&It.has(n)){t?this.flowKey&&(this.flowKey=!1):this.indentNext=this.indentValue+1,e+=yield*this.pushCount(1),e+=yield*this.pushSpaces(!0);continue e}}}break e}return e}*pushTag(){if(this.charAt(1)==="<"){let e=this.pos+2,t=this.buffer[e];for(;!W(t)&&t!==">";)t=this.buffer[++e];return yield*this.pushToIndex(t===">"?e+1:e,!1)}else{let e=this.pos+1,t=this.buffer[e];for(;t;)if(Pn.has(t))t=this.buffer[++e];else if(t==="%"&&Gs.has(this.buffer[e+1])&&Gs.has(this.buffer[e+2]))t=this.buffer[e+=3];else break;return yield*this.pushToIndex(e,!1)}}*pushNewline(){let e=this.buffer[this.pos];return e===`
`?yield*this.pushCount(1):e==="\r"&&this.charAt(1)===`
`?yield*this.pushCount(2):0}*pushSpaces(e){let t=this.pos-1,n;do n=this.buffer[++t];while(n===" "||e&&n===" ");let i=t-this.pos;return i>0&&(yield this.buffer.substr(this.pos,i),this.pos=t),i}*pushUntil(e){let t=this.pos,n=this.buffer[t];for(;!e(n);)n=this.buffer[++t];return yield*this.pushToIndex(t,!1)}};var Qe=class{constructor(){this.lineStarts=[],this.addNewLine=e=>this.lineStarts.push(e),this.linePos=e=>{let t=0,n=this.lineStarts.length;for(;t<n;){let r=t+n>>1;this.lineStarts[r]<e?t=r+1:n=r}if(this.lineStarts[t]===e)return{line:t+1,col:1};if(t===0)return{line:0,col:e};let i=this.lineStarts[t-1];return{line:t,col:e-i+1}}}};function me(s,e){for(let t=0;t<s.length;++t)if(s[t].type===e)return!0;return!1}function Ws(s){for(let e=0;e<s.length;++e)switch(s[e].type){case"space":case"comment":case"newline":break;default:return e}return-1}function Xs(s){switch(s?.type){case"alias":case"scalar":case"single-quoted-scalar":case"double-quoted-scalar":case"flow-collection":return!0;default:return!1}}function Lt(s){switch(s.type){case"document":return s.start;case"block-map":{let e=s.items[s.items.length-1];return e.sep??e.start}case"block-seq":return s.items[s.items.length-1].start;default:return[]}}function Ce(s){if(s.length===0)return[];let e=s.length;e:for(;--e>=0;)switch(s[e].type){case"doc-start":case"explicit-key-ind":case"map-value-ind":case"seq-item-ind":case"newline":break e}for(;s[++e]?.type==="space";);return s.splice(e,s.length)}function Ct(s,e){if(e.length<1e5)Array.prototype.push.apply(s,e);else for(let t=0;t<e.length;++t)s.push(e[t])}function Hs(s){if(s.start.type==="flow-seq-start")for(let e of s.items)e.sep&&!e.value&&!me(e.start,"explicit-key-ind")&&!me(e.sep,"map-value-ind")&&(e.key&&(e.value=e.key),delete e.key,Xs(e.value)?e.value.end?Ct(e.value.end,e.sep):e.value.end=e.sep:Ct(e.start,e.sep),delete e.sep)}var Ge=class{constructor(e){this.atNewLine=!0,this.atScalar=!1,this.indent=0,this.offset=0,this.onKeyLine=!1,this.stack=[],this.source="",this.type="",this.lexer=new Ye,this.onNewLine=e}*parse(e,t=!1){this.onNewLine&&this.offset===0&&this.onNewLine(0);for(let n of this.lexer.lex(e,t))yield*this.next(n);t||(yield*this.end())}*next(e){if(this.source=e,this.atScalar){this.atScalar=!1,yield*this.step(),this.offset+=e.length;return}let t=Qs(e);if(t)if(t==="scalar")this.atNewLine=!1,this.atScalar=!0,this.type="scalar";else{switch(this.type=t,yield*this.step(),t){case"newline":this.atNewLine=!0,this.indent=0,this.onNewLine&&this.onNewLine(this.offset+e.length);break;case"space":this.atNewLine&&e[0]===" "&&(this.indent+=e.length);break;case"explicit-key-ind":case"map-value-ind":case"seq-item-ind":this.atNewLine&&(this.indent+=e.length);break;case"doc-mode":case"flow-error-end":return;default:this.atNewLine=!1}this.offset+=e.length}else{let n=`Not a YAML token: ${e}`;yield*this.pop({type:"error",offset:this.offset,message:n,source:e}),this.offset+=e.length}}*end(){for(;this.stack.length>0;)yield*this.pop()}get sourceToken(){return{type:this.type,offset:this.offset,indent:this.indent,source:this.source}}*step(){let e=this.peek(1);if(this.type==="doc-end"&&e?.type!=="doc-end"){for(;this.stack.length>0;)yield*this.pop();this.stack.push({type:"doc-end",offset:this.offset,source:this.source});return}if(!e)return yield*this.stream();switch(e.type){case"document":return yield*this.document(e);case"alias":case"scalar":case"single-quoted-scalar":case"double-quoted-scalar":return yield*this.scalar(e);case"block-scalar":return yield*this.blockScalar(e);case"block-map":return yield*this.blockMap(e);case"block-seq":return yield*this.blockSequence(e);case"flow-collection":return yield*this.flowCollection(e);case"doc-end":return yield*this.documentEnd(e)}yield*this.pop()}peek(e){return this.stack[this.stack.length-e]}*pop(e){let t=e??this.stack.pop();if(!t)yield{type:"error",offset:this.offset,source:"",message:"Tried to pop an empty stack"};else if(this.stack.length===0)yield t;else{let n=this.peek(1);switch(t.type==="block-scalar"?t.indent="indent"in n?n.indent:0:t.type==="flow-collection"&&n.type==="document"&&(t.indent=0),t.type==="flow-collection"&&Hs(t),n.type){case"document":n.value=t;break;case"block-scalar":n.props.push(t);break;case"block-map":{let i=n.items[n.items.length-1];if(i.value){n.items.push({start:[],key:t,sep:[]}),this.onKeyLine=!0;return}else if(i.sep)i.value=t;else{Object.assign(i,{key:t,sep:[]}),this.onKeyLine=!i.explicitKey;return}break}case"block-seq":{let i=n.items[n.items.length-1];i.value?n.items.push({start:[],value:t}):i.value=t;break}case"flow-collection":{let i=n.items[n.items.length-1];!i||i.value?n.items.push({start:[],key:t,sep:[]}):i.sep?i.value=t:Object.assign(i,{key:t,sep:[]});return}default:yield*this.pop(),yield*this.pop(t)}if((n.type==="document"||n.type==="block-map"||n.type==="block-seq")&&(t.type==="block-map"||t.type==="block-seq")){let i=t.items[t.items.length-1];i&&!i.sep&&!i.value&&i.start.length>0&&Ws(i.start)===-1&&(t.indent===0||i.start.every(r=>r.type!=="comment"||r. Line truncated
`)+1;for(;t!==0;)this.onNewLine(this.offset+t),t=this.source.indexOf(`
`,t)+1}yield*this.pop();break;default:yield*this.pop(),yield*this.step()}}*blockMap(e){let t=e.items[e.items.length-1];switch(this.type){case"newline":if(this.onKeyLine=!1,t.value){let n="end"in t.value?t.value.end:void 0;(Array.isArray(n)?n[n.length-1]:void 0)?.type==="comment"?n?.push(this.sourceToken):e.items.push({start:[this.sourceToken]})}else t.sep?t.sep.push(this.sourceToken):t.start.push(this.sourceToken);return;case"space":case"comment":if(t.value)e.items.push({start:[this.sourceToken]});else if(t.sep)t.sep.push(this.sourceToken);else{if(this.atIndentedComment(t.start,e.indent)){let i=e.items[e.items.length-2]?.value?.end;if(Array.isArray(i)){Ct(i,t.start),i.push(this.sourceToken),e.items.pop();return}}t.start.push(this.sourceToken)}return}if(this.indent>=e.indent){let n=!this.onKeyLine&&this.indent===e.indent,i=n&&(t.sep||t.explicitKey)&&this.type!=="seq-item-ind",r=[];if(i&&t.sep&&!t.value){let o=[];for(let a=0;a<t.sep.length;++a){let l=t.sep[a];switch(l.type){case"newline":o.push(a);break;case"space":break;case"comment":l.indent>e.indent&&(o.length=0);break;default:o.length=0}}o.length>=2&&(r=t.sep.splice(o[1]))}switch(this.type){case"anchor":case"tag":i||t.value?(r.push(this.sourceToken),e.items.push({start:r}),this.onKeyLine=!0):t.sep?t.sep.push(this.sourceToken):t.start.push(this.sourceToken);return;case"explicit-key-ind":!t.sep&&!t.explicitKey?(t.start.push(this.sourceToken),t.explicitKey=!0):i||t.value?(r.push(this.sourceToken),e.items.push({start:r,explicitKey:!0})):this.stack.push({type:"block-map",offset:this.offset,indent:this.indent,items:[{start:[this.sourceToken],explicitKey:!0}]}),this.onKeyLine=!0;return;case"map-value-ind":if(t.explicitKey)if(t.sep)if(t.value)e.items.push({start:[],key:null,sep:[this.sourceToken]});else if(me(t.sep,"map-value-ind"))this.stack.push({type:"block-map",offset:this.offset,indent:this.indent,items:[{start:r,key:null,sep:[this.sourceToken]}]});else if(Xs(t.key)&&!me(t.sep,"newline")){let o=Ce(t.start),a=t.key,l=t.sep;l.push(this.sourceToken),delete t.key,delete t.sep,this.stack.push({type:"block-map",offset:this.offset,indent:this.indent,items:[{start:o,key:a,sep:l}]})}else r.length>0?t.sep=t.sep.concat(r,this.sourceToken):t.sep.push(this.sourceToken);else if(me(t.start,"newline"))Object.assign(t,{key:null,sep:[this.sourceToken]});else{let o=Ce(t.start);this.stack.push({type:"block-map",offset:this.offset,indent:this.indent,items:[{start:o,key:null,sep:[this.sourceToken]}]})}else t.sep?t.value||i?e.items.push({start:r,key:null,sep:[this.sourceToken]}):me(t.sep,"map-value-ind")?this.stack.push({type:"block-map",offset:this.offset,indent:this.indent,items:[{start:[],key:null,sep:[this.sourceToken]}]}):t.sep.push(this.sourceToken):Object.assign(t,{key:null,sep:[this.sourceToken]});this.onKeyLine=!0;return;case"alias":case"scalar":case"single-quoted-scalar":case"double-quoted-scalar":{let o=this.flowScalar(this.type);i||t.value?(e.items.push({start:r,key:o,sep:[]}),this.onKeyLine=!0):t.sep?this.stack.push(o):(Object.assign(t,{key:o,sep:[]}),this.onKeyLine=!0);return}default:{let o=this.startBlockValue(e);if(o){if(o.type==="block-seq"){if(!t.explicitKey&&t.sep&&!me(t.sep,"newline")){yield*this.pop({type:"error",offset:this.offset,message:"Unexpected block-seq-ind on same line with key",source:this.source});return}}else n&&e.items.push({start:r});this.stack.push(o);return}}}}yield*this.pop(),yield*this.step()}*blockSequence(e){let t=e.items[e.items.length-1];switch(this.type){case"newline":if(t.value){let n="end"in t.value?t.value.end:void 0;(Array.isArray(n)?n[n.length-1]:void 0)?.type==="comment"?n?.push(this.sourceToken):e.items.push({start:[this.sourceToken]})}else t.start.push(this.sourceToken);return;case"space":case"comment":if(t.value)e.items.push({start:[this.sourceToken]});else{if(this.atIndentedComment(t.start,e.indent)){let i=e.items[e.items.length-2]?.value?.end;if(Array.isArray(i)){Ct(i,t.start),i.push(this.sourceToken),e.items.pop();return}}t.start.push(this.sourceToken)}return;case"anchor":case"tag":if(t.value||this.indent<=e.indent)break;t.start.push(this.sourceToken);return;case"seq-item-ind":if(this.indent!==e.indent)break;t.value||me(t.start,"seq-item-ind")?e.items.push({start:[this.sourceToken]}):t.start.push(this.sourceToken);return}if(this.indent>e.indent){let n=this.startBlockValue(e);if(n){this.stack.push(n);return}}yield*this.pop(),yield*this.step()}*flowCollection(e){let t=e.items[e.items.length-1];if(this.type==="flow-error-end"){let n;do yield*this.pop(),n=this.peek(1);while(n?.type==="flow-collection")}else if(e.end.length===0){switch(this.type){case"comma":case"explicit-key-ind":!t||t.sep?e.items.push({start:[this.sourceToken]}):t.start.push(this.sourceToken);return;case"map-value-ind":!t||t.value?e.items.push({start:[],key:null,sep:[this.sourceToken]}):t.sep?t.sep.push(this.sourceToken):Object.assign(t,{key:null,sep:[this.sourceToken]});return;case"space":case"comment":case"newline":case"anchor":case"tag":!t||t.value?e.items.p Line truncated
`)+1;for(;t!==0;)this.onNewLine(this.offset+t),t=this.source.indexOf(`
`,t)+1}return{type:e,offset:this.offset,indent:this.indent,source:this.source}}startBlockValue(e){switch(this.type){case"alias":case"scalar":case"single-quoted-scalar":case"double-quoted-scalar":return this.flowScalar(this.type);case"block-scalar-header":return{type:"block-scalar",offset:this.offset,indent:this.indent,props:[this.sourceToken],source:""};case"flow-map-start":case"flow-seq-start":return{type:"flow-collection",offset:this.offset,indent:this.indent,start:this.sourceToken,items:[],end:[]};case"seq-item-ind":return{type:"block-seq",offset:this.offset,indent:this.indent,items:[{start:[this.sourceToken]}]};case"explicit-key-ind":{this.onKeyLine=!0;let t=Lt(e),n=Ce(t);return n.push(this.sourceToken),{type:"block-map",offset:this.offset,indent:this.indent,items:[{start:n,explicitKey:!0}]}}case"map-value-ind":{this.onKeyLine=!0;let t=Lt(e),n=Ce(t);return{type:"block-map",offset:this.offset,indent:this.indent,items:[{start:n,key:null,sep:[this.sourceToken]}]}}}return null}atIndentedComment(e,t){return this.type!=="comment"||this.indent<=t?!1:e.every(n=>n.type==="newline"||n.type==="space")}*documentEnd(e){this.type!=="doc-mode"&&(e.end?e.end.push(this.sourceToken):e.end=[this.sourceToken],this.type==="newline"&&(yield*this.pop()))}*lineEnd(e){switch(this.type){case"comma":case"doc-start":case"doc-end":case"flow-seq-end":case"flow-map-end":case"map-value-ind":yield*this.pop(),yield*this.step();break;case"newline":this.onKeyLine=!1;default:e.end?e.end.push(this.sourceToken):e.end=[this.sourceToken],this.type==="newline"&&(yield*this.pop())}}};function Bn(s){let e=s.prettyErrors!==!1;return{lineCounter:s.lineCounter||e&&new Qe||null,prettyErrors:e}}function as(s,e={}){let{lineCounter:t,prettyErrors:n}=Bn(e),i=new Ge(t?.addNewLine),r=new Je(e),o=null;for(let a of r.compose(i.parse(s),!0,s.length))if(!o)o=a;else if(o.options.logLevel!=="silent"){o.errors.push(new G(a.range.slice(0,2),"MULTIPLE_DOCS","Source contains multiple documents; please use YAML.parseAllDocuments()"));break}return n&&t&&(o.errors.forEach(Gt(s,t)),o.warnings.forEach(Gt(s,t))),o}return nn(Kn);})();
return baseclass.extend(MeowYaml);
@@ -0,0 +1,337 @@
'use strict';
'require view';
'require fs';
'require network';
'require rpc';
'require uci';
'require ui';
// LAN clients known to this router. The main control is a per-client proxy
// bypass: ticked MACs are stored as `tproxy.bypass_mac` in /etc/config/meow and
// gateway.sh returns their traffic before both the proxy capture and the DNS
// hijack. ARP-based client steering (ARP spoofing, `arp.client`, enforced by
// the meow-arp service) is an opt-in advanced section; its Steer column is only
// shown while steering is enabled.
// MAC -> { name, ipaddrs, ip6addrs }: DHCP leases, static hosts, /etc/ethers
// and neighbours merged by LuCI, so side routers without DHCP still get rows.
var callHostHints = rpc.declare({
object: 'luci-rpc',
method: 'getHostHints',
expect: { '': {} }
});
var callDHCPLeases = rpc.declare({
object: 'luci-rpc',
method: 'getDHCPLeases',
expect: { '': {} }
});
function macList(section, option) {
var l = uci.get('meow', section, option);
if (l == null) return [];
return (Array.isArray(l) ? l : [ l ]).map(function(m) { return m.toLowerCase(); });
}
// MACs of static leases (`config host` in /etc/config/dhcp). `mac` may be a
// list or a space-separated string of several MACs.
function staticMacs() {
var out = {};
uci.sections('dhcp', 'host', function(s) {
var m = s.mac;
(Array.isArray(m) ? m : String(m || '').split(/\s+/)).forEach(function(x) {
if (x) out[x.toLowerCase()] = true;
});
});
return out;
}
function ip4ToInt(ip) {
var p = String(ip).split('.');
if (p.length !== 4) return null;
return ((+p[0] << 24) | (+p[1] << 16) | (+p[2] << 8) | +p[3]) >>> 0;
}
// ['192.168.1.1/24', ...] -> [{ net, mask }] for the proxied LAN interface.
function parseSubnets(cidrs) {
return (cidrs || []).map(function(c) {
var parts = String(c).split('/'), ip = ip4ToInt(parts[0]);
var bits = parts.length > 1 ? +parts[1] : 32;
if (ip === null || !(bits >= 0 && bits <= 32)) return null;
var mask = bits ? (0xffffffff << (32 - bits)) >>> 0 : 0;
return { self: ip, net: (ip & mask) >>> 0, mask: mask };
}).filter(Boolean);
}
function inSubnets(ip, subnets) {
var n = ip4ToInt(ip);
return n !== null && subnets.some(function(s) {
return n !== s.self && ((n & s.mask) >>> 0) === s.net;
});
}
// IPv4 subnets of the interface transparent proxying listens on (tproxy.interface).
function lanSubnets() {
var iface = uci.get('meow', 'tproxy', 'interface') || 'lan';
return L.resolveDefault(network.getNetwork(iface), null).then(function(net) {
return net ? net.getIPAddrs() : [];
}).catch(function() { return []; });
}
return view.extend({
load: function() {
return uci.load('meow').then(function() {
return Promise.all([
null,
uci.load('dhcp').catch(function() {}),
L.resolveDefault(callHostHints(), {}),
L.resolveDefault(callDHCPLeases(), {}),
lanSubnets()
]);
});
},
// One row per LAN client, including selected MACs that are not currently
// seen so they can still be released. Host hints also cover WAN-side
// neighbours, the router itself, the all-zero MAC and IPv6-link-local-only
// peers; a hint is kept only with an IPv4 on the LAN subnet other than the
// router's own address (any IPv4 when the subnet is unknown). DHCP leases and selected MACs are always kept.
buildClients: function(hints, leases, statics, bypass, steer, cidrs) {
var byMac = {};
var subnets = parseSubnets(cidrs);
function row(mac) {
return byMac[mac] || (byMac[mac] = {
mac: mac, name: '', ip4: [], ip6: [], source: 'neighbour'
});
}
Object.keys(hints || {}).forEach(function(m) {
var h = hints[m] || {};
var ip4 = (h.ipaddrs || h.ipv4 || []).filter(function(ip) {
return ip && (!subnets.length || inSubnets(ip, subnets));
});
if (!ip4.length || /^(00:){5}00$/i.test(m)) return;
var r = row(m.toLowerCase());
r.name = h.name || '';
r.ip4 = ip4;
r.ip6 = (h.ip6addrs || h.ipv6 || []).slice();
});
((leases || {}).dhcp_leases || []).forEach(function(l) {
if (!l.macaddr) return;
var r = row(l.macaddr.toLowerCase());
r.source = 'dhcp';
if (!r.name && l.hostname && l.hostname !== '*') r.name = l.hostname;
if (l.ipaddr && r.ip4.indexOf(l.ipaddr) === -1) r.ip4.unshift(l.ipaddr);
});
Object.keys(statics || {}).forEach(function(m) {
if (byMac[m] && byMac[m].source !== 'dhcp') byMac[m].source = 'static';
});
bypass.concat(steer).forEach(function(m) {
if (!byMac[m]) row(m).source = 'offline';
});
return Object.keys(byMac).map(function(m) { return byMac[m]; })
.sort(function(a, b) {
var ao = a.source === 'offline', bo = b.source === 'offline';
if (ao !== bo) return ao ? 1 : -1;
if (!a.name !== !b.name) return a.name ? -1 : 1;
return a.name.localeCompare(b.name) ||
(a.ip4[0] || '').localeCompare(b.ip4[0] || '', undefined, { numeric: true }) ||
a.mac.localeCompare(b.mac);
});
},
render: function(data) {
var self = this;
var statics = staticMacs();
var bypass = macList('tproxy', 'bypass_mac');
var steer = macList('arp', 'client');
var arpEnabled = uci.get('meow', 'arp', 'enabled') === '1';
var tproxyEnabled = uci.get('meow', 'tproxy', 'enabled') === '1';
var clients = this.buildClients(data[2], data[3], statics, bypass, steer, data[4]);
var hidden = 'display: none;';
var sourceLabel = {
dhcp: _('DHCP'), static: _('Static'), neighbour: _('Neighbour'), offline: _('Offline')
};
this.bypassChecks = {};
this.steerChecks = {};
// Steer cells and the DHCP-only empty state follow the ARP toggle.
// Neighbour rows also stay visible when their current Bypass tick is set.
this.arpOnly = [];
this.dhcpOnly = [];
this.neighbourRows = [];
this.initial = { arpEnabled: arpEnabled, steer: steer.slice().sort().join(' ') };
function arpOnly(node) { self.arpOnly.push(node); return node; }
function steerCell(tag, content) {
return arpOnly(E(tag, { 'class': tag === 'th' ? 'th' : 'td', 'style': arpEnabled ? '' : hidden }, content));
}
// Without ARP steering only DHCP clients (leases and static leases) are
// listed; neighbour-only hosts stay visible when already bypassed.
function dhcpVisible(c) {
return c.source !== 'neighbour' || bypass.indexOf(c.mac) !== -1;
}
var rows = [
E('tr', { 'class': 'tr table-titles' }, [
E('th', { 'class': 'th', 'style': 'width: 4em;' }, _('Bypass')),
steerCell('th', _('Steer')),
E('th', { 'class': 'th' }, _('Hostname')),
E('th', { 'class': 'th' }, _('IP address')),
E('th', { 'class': 'th' }, _('MAC address')),
E('th', { 'class': 'th' }, _('Source'))
])
];
if (!clients.length) {
rows.push(E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td', 'colspan': '6' },
E('em', {}, _('No LAN clients known yet. DHCP leases and neighbours appear here once clients are active.')))
]));
} else if (!clients.some(dhcpVisible)) {
rows.push(this.dhcpOnly[this.dhcpOnly.push(E('tr', { 'class': 'tr', 'style': arpEnabled ? hidden : '' }, [
E('td', { 'class': 'td', 'colspan': '6' },
E('em', {}, _('No DHCP clients. Enable ARP client steering below to list other LAN neighbours.')))
])) - 1]);
}
clients.forEach(function(c) {
// Bypass and Steer are exclusive: bypassing a steered client would
// only route it through this router unproxied.
var bp = E('input', {
'type': 'checkbox',
'checked': bypass.indexOf(c.mac) !== -1 ? '' : null,
'change': function() { if (bp.checked) st.checked = false; }
});
var st = E('input', {
'type': 'checkbox',
'checked': steer.indexOf(c.mac) !== -1 ? '' : null,
'change': function() { if (st.checked) bp.checked = false; }
});
self.bypassChecks[c.mac] = bp;
self.steerChecks[c.mac] = st;
var ips = [ E('span', {}, c.ip4.length ? c.ip4.join(', ') : '-') ];
if (c.ip6.length)
ips.push(E('br'), E('small', { 'style': 'color: #888;' }, c.ip6.join(', ')));
var tr = E('tr', { 'class': 'tr', 'style': arpEnabled || dhcpVisible(c) ? '' : hidden }, [
E('td', { 'class': 'td' }, bp),
steerCell('td', st),
E('td', { 'class': 'td' }, c.name || '-'),
E('td', { 'class': 'td' }, ips),
E('td', { 'class': 'td' }, c.mac),
E('td', { 'class': 'td' }, sourceLabel[c.source])
]);
rows.push(tr);
if (c.source === 'neighbour')
self.neighbourRows.push({ node: tr, bypass: bp });
});
var intro = E('p', {}, [
_('Tick a client to bypass the transparent proxy: its traffic (including DNS) ' +
'goes out directly. Clients are matched by MAC address, so a new DHCP ' +
'lease keeps the setting.'),
tproxyEnabled ? '' : E('strong', {}, [ ' ',
_('Transparent proxy is disabled (Settings), so no client is proxied right now.') ])
]);
this.arpToggle = E('input', {
'type': 'checkbox',
'id': 'arp-enabled',
'checked': arpEnabled ? '' : null,
'change': function(ev) { self.showSteer(ev.target.checked); }
});
var arp = E('details', { 'class': 'cbi-section', 'open': arpEnabled ? '' : null }, [
E('summary', {}, E('strong', {}, _('ARP client steering (advanced)'))),
E('div', { 'class': 'alert-message warning', 'style': 'margin: 1em 0;' }, [
_('For the clients you tick in the Steer column, this router announces itself ' +
'as their gateway (ARP spoofing) so meow transparently proxies them without ' +
'touching the client or the main router. Only use it for devices you ' +
'administer on a network you control. Untick a client to release it. ' +
'Transparent proxy must be enabled (Settings) for steered traffic to be handled.')
]),
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title', 'for': 'arp-enabled' }, _('Enable steering')),
E('div', { 'class': 'cbi-value-field' }, [
this.arpToggle,
E('span', { 'style': 'margin-left: .5em; color: #888;' },
_('Master switch. Shows the Steer column and non-DHCP LAN neighbours; when off, no client is steered.'))
])
])
]);
return E('div', { 'class': 'cbi-map' }, [
E('h2', {}, _('meow Clients')),
intro,
E('div', { 'class': 'cbi-section' }, [
E('table', { 'class': 'table cbi-section-table' }, rows)
]),
// Saving uses LuCI's standard page footer (Save & Apply).
E('div', { 'style': 'margin: .5em 0;' }, [
E('button', {
'class': 'cbi-button',
'click': function() { location.reload(); }
}, _('Reload list'))
]),
arp
]);
},
showSteer: function(on) {
this.arpOnly.forEach(function(n) { n.style.display = on ? '' : 'none'; });
this.neighbourRows.forEach(function(row) {
row.node.style.display = on || row.bypass.checked ? '' : 'none';
});
var hasBypass = this.neighbourRows.some(function(row) { return row.bypass.checked; });
this.dhcpOnly.forEach(function(n) { n.style.display = on || hasBypass ? 'none' : ''; });
},
handleSaveApply: function() {
var self = this;
var arpEnabled = this.arpToggle.checked;
function ticked(checks) {
return Object.keys(checks).filter(function(m) { return checks[m].checked; });
}
var bypass = ticked(this.bypassChecks);
// Hidden Steer ticks are kept, so disabling steering does not forget them.
var steer = ticked(this.steerChecks).filter(function(m) { return bypass.indexOf(m) === -1; });
var arpChanged = arpEnabled !== this.initial.arpEnabled ||
steer.slice().sort().join(' ') !== this.initial.steer;
if (bypass.length)
uci.set('meow', 'tproxy', 'bypass_mac', bypass);
else
uci.unset('meow', 'tproxy', 'bypass_mac');
uci.set('meow', 'arp', 'enabled', arpEnabled ? '1' : '0');
if (steer.length)
uci.set('meow', 'arp', 'client', steer);
else
uci.unset('meow', 'arp', 'client');
return uci.save()
.then(function() { return uci.apply(); })
.then(function() {
if (arpChanged)
return fs.exec('/etc/init.d/meow-arp', [ 'restart' ]).catch(function() {});
})
.then(function() {
ui.addTimeLimitedNotification(null, E('p', arpEnabled
? _('Bypassing %d client(s); steering %d.').format(bypass.length, steer.length)
: _('Bypassing %d client(s); steering disabled.').format(bypass.length)), 4000, 'info');
})
.catch(function(e) {
ui.addNotification(null, E('p', _('Failed to apply: %s').format(e.message)));
});
},
handleSave: null,
handleReset: null
});
@@ -0,0 +1,225 @@
'use strict';
'require view';
'require fs';
'require request';
'require ui';
'require uci';
'require tools.meow_settings as settings';
'require tools.meow as meow';
// Raw editor for the meow YAML configuration. Edits are validated with
// `meow -t` against a scratch copy before they replace the real file. A running
// service is explicitly restarted after saving. Runtime changes through the
// panel are written back to this file by its "Save Config" button.
// YAML subscriptions can exceed ubus request limits. Use the same authenticated
// multipart upload as LuCI's file picker; cgi-io enforces the file ACLs.
function writeConfig(path, content) {
var data = new FormData();
data.append('sessionid', L.env.sessionid);
data.append('filename', path);
data.append('filedata', new Blob([ content ], { type: 'text/plain' }), 'config.yaml');
return request.post(L.env.cgi_base + '/cgi-upload', data, { timeout: 0 }).then(function(res) {
if (!res.ok)
throw new Error(res.statusText || _('Upload request failed'));
var reply = res.json();
if (!reply || reply.failure)
throw new Error((reply && reply.message) || _('Upload request failed'));
});
}
return view.extend({
load: function() {
return uci.load('meow').then(function() {
var path = uci.get('meow', 'main', 'config_file') || '/etc/meow/config.yaml';
return fs.read_direct(path).then(function(content) {
return { path: path, content: content };
});
});
},
validate: function(content) {
// Isolate simultaneous saves/validations, including other browser tabs.
// Reuse the token path supported by the validator and upload ACL.
var token = Array.from(crypto.getRandomValues(new Uint32Array(4)), function(n) {
return n.toString(16).padStart(8, '0');
}).join('');
var scratch = '/tmp/meow-luci-settings-' + token + '.yaml';
return writeConfig(scratch, content).then(function() {
return fs.exec('/usr/libexec/meow-validate', [token]);
}).then(function(res) {
if (res.code === 0)
return null;
// meow logs to stdout with ANSI colors; keep the error lines.
var out = ((res.stdout || '') + (res.stderr || ''))
.replace(/\x1b\[[0-9;]*m/g, '').trim().split('\n');
var errors = out.filter(function(l) { return /ERROR|Error/.test(l); });
return (errors.length ? errors : out).join('\n') || _('Configuration test failed');
}).finally(function() {
return fs.remove(scratch).catch(function() {});
});
},
handleValidate: function() {
var content = document.getElementById('meow-yaml').value;
return Promise.resolve().then(function() {
content = settings.prepare(content);
return this.validate(content);
}.bind(this)).then(function(err) {
if (err)
ui.addNotification(_('Invalid configuration'), E('pre', {}, err), 'error');
else
ui.addTimeLimitedNotification(null, E('p', _('Configuration test passed')), 3000, 'info');
}).catch(function(e) {
ui.addNotification(null, E('p', _('Unable to validate: %s').format(e.message)), 'error');
});
},
handleSave: function(ev, path) {
if (this.saving) return this.saving;
var self = this;
var textarea = document.getElementById('meow-yaml');
var original = textarea.value;
var content = original.replace(/\r\n/g, '\n');
if (!/\n$/.test(content))
content += '\n';
return this.saving = Promise.resolve().then(function() {
content = settings.prepare(content);
return this.validate(content);
}.bind(this)).then(function(err) {
if (err) {
ui.addNotification(_('Invalid configuration, not saved'), E('pre', {}, err), 'error');
return;
}
return writeConfig(path, content).then(function() {
// Keep edits entered while validation/upload was in progress.
if (textarea.value === original) textarea.value = content;
self.saved = content;
self.updateStatus();
return meow.serviceRunning().then(function(running) {
if (!running) return false;
return fs.exec('/etc/init.d/meow', ['restart']).then(function(res) {
if (res.code !== 0) throw new Error(res.stderr || _('Service restart failed'));
return meow.serviceRunning().then(function(active) {
if (!active) throw new Error(_('Service did not start'));
return true;
});
});
}).then(function(restarted) {
ui.addTimeLimitedNotification(null, E('p', restarted
? _('Configuration saved and service restarted.')
: _('Configuration saved; service remains stopped.')), 5000, 'info');
}).catch(function(error) {
ui.addNotification(null, E('p', _('Configuration saved, but restart failed: %s').format(error.message)), 'error');
});
});
}).catch(function(e) {
ui.addNotification(null, E('p', _('Unable to save: %s').format(e.message)));
}).finally(function() {
self.saving = null;
});
},
// Editor status line: size, line count and whether the text differs from
// the file on disk. `saved` tracks the last content written or loaded.
updateStatus: function(text) {
if (text == null) {
var ta = document.getElementById('meow-yaml');
if (!ta) return;
text = ta.value;
}
if (!this.status) return;
this.dirty = text !== this.saved;
var lines = text ? text.split('\n').length : 0;
this.status.textContent = _('%d lines · %s').format(lines, '%1024.1mB'.format(text.length)) +
' · ' + (this.dirty ? _('Unsaved changes') : _('Saved'));
this.status.style.color = this.dirty ? '#ef6c00' : '#888';
if (this.revert) this.revert.disabled = !this.dirty;
},
handleRevert: function() {
var ta = document.getElementById('meow-yaml');
ta.value = this.saved;
this.updateStatus();
},
// Tab indents with two spaces (YAML forbids tabs); Ctrl/Cmd+S saves.
handleKey: function(path, ev) {
var ta = ev.target;
if (ev.key === 'Tab' && !ev.ctrlKey && !ev.metaKey && !ev.altKey) {
ev.preventDefault();
var start = ta.selectionStart, end = ta.selectionEnd;
ta.value = ta.value.slice(0, start) + ' ' + ta.value.slice(end);
ta.selectionStart = ta.selectionEnd = start + 2;
this.updateStatus();
} else if (ev.key === 's' && (ev.ctrlKey || ev.metaKey)) {
ev.preventDefault();
this.handleSave(ev, path);
}
},
render: function(data) {
var self = this;
this.saved = data.content;
this.status = E('span', { 'style': 'color: #888;' });
// Leaving with unsaved edits asks first.
if (typeof window !== 'undefined' && window.addEventListener)
window.addEventListener('beforeunload', function(ev) {
if (self.dirty) { ev.preventDefault(); ev.returnValue = ''; }
});
this.revert = E('button', {
'type': 'button',
'class': 'cbi-button cbi-button-reset',
'disabled': true,
// Plain handler: createHandlerFn would re-enable the button afterwards.
'click': function() { self.handleRevert(); }
}, _('Revert'));
var node = E('div', { 'class': 'cbi-map' }, [
E('h2', {}, _('meow Configuration')),
E('div', { 'class': 'cbi-map-descr' }, [
_('Raw YAML configuration at %s (mihomo / Clash Meta format). ' +
'Settings from the Settings tab are applied and the result is validated before saving; ' +
'an invalid configuration is never written. Tab indents, Ctrl/Cmd+S saves.').format(data.path)
]),
E('div', { 'class': 'cbi-section' }, [
E('textarea', {
'id': 'meow-yaml',
'class': 'cbi-input-textarea',
'style': 'width: 100%; min-height: 60vh; font-family: monospace; font-size: 12px; tab-size: 2;',
'spellcheck': 'false',
'wrap': 'off',
'input': function() { self.updateStatus(); },
'keydown': function(ev) { self.handleKey(data.path, ev); }
}, [ data.content ]),
E('div', { 'style': 'margin-top: .3em; font-size: 12px;' }, [ this.status ])
]),
E('div', { 'class': 'cbi-page-actions' }, [
this.revert,
' ',
E('button', {
'type': 'button',
'class': 'cbi-button cbi-button-neutral',
'click': ui.createHandlerFn(this, 'handleValidate')
}, _('Validate')),
' ',
E('button', {
'type': 'button',
'class': 'cbi-button cbi-button-save',
'click': ui.createHandlerFn(this, 'handleSave', null, data.path)
}, _('Save'))
])
]);
this.updateStatus(data.content);
return node;
},
handleSaveApply: null,
handleReset: null,
addFooter: function() { return E('div'); }
});
@@ -0,0 +1,135 @@
'use strict';
'require view';
'require dom';
'require fs';
'require poll';
'require ui';
// meow runs under procd with stdout/stderr captured into the system log.
var MAX_LINES = 1000;
var LEVELS = {
all: function() { return true; },
warn: function(l) { return level(l) !== ''; },
error: function(l) { return level(l) === 'error'; }
};
function level(line) {
if (/\bERROR\b|\.err\b|\.crit\b|panicked/.test(line)) return 'error';
if (/\bWARN\b|\.warn\b/.test(line)) return 'warn';
return '';
}
function fetchLog() {
return fs.exec_direct('/sbin/logread', [ '-e', 'meow' ]).then(function(out) {
var lines = (out || '').replace(/\x1b\[[0-9;]*m/g, '').trim().split('\n');
return lines.filter(Boolean).slice(-MAX_LINES);
}).catch(function(e) {
return [ _('Unable to read log: %s').format(e.message) ];
});
}
return view.extend({
load: fetchLog,
// Oldest first, errors and warnings highlighted. Follows the tail unless
// the reader has scrolled up.
show: function() {
var pre = this.pre, filter = (this.filter.value || '').toLowerCase();
var match = LEVELS[this.levelSel.value] || LEVELS.all;
var lines = this.lines.filter(function(l) {
return match(l) && (!filter || l.toLowerCase().indexOf(filter) !== -1);
});
var atBottom = pre.scrollHeight - pre.scrollTop - pre.clientHeight < 40;
dom.content(pre, lines.length ? lines.map(function(l) {
var lv = level(l);
return E('div', { 'style': lv === 'error' ? 'color: #c62828;' : lv === 'warn' ? 'color: #ef6c00;' : '' }, l);
}) : E('em', {}, this.lines.length ? _('No entries match the filter.') : _('No log entries.')));
dom.content(this.count, _('%d of %d entries').format(lines.length, this.lines.length));
// The first render happens before the node is attached (no height yet);
// jump to the newest entries once it is on the page.
if (atBottom || !this.scrolled) {
pre.scrollTop = pre.scrollHeight;
if (pre.isConnected && pre.scrollHeight > 0) this.scrolled = true;
}
},
refresh: function() {
if (this.paused) return Promise.resolve();
return fetchLog().then(L.bind(function(lines) {
this.lines = lines;
this.show();
}, this));
},
handlePause: function() {
this.paused = !this.paused;
this.pauseBtn.textContent = this.paused ? _('Resume') : _('Pause');
this.pauseBtn.classList.toggle('cbi-button-positive', this.paused);
if (!this.paused) return this.refresh();
},
handleDownload: function() {
var blob = new Blob([ this.lines.join('\n') + '\n' ], { type: 'text/plain' });
var a = E('a', { 'href': URL.createObjectURL(blob), 'download': 'meow.log' });
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(a.href);
},
render: function(lines) {
var self = this;
this.lines = lines || [];
this.pre = E('pre', {
'style': 'white-space: pre-wrap; font-size: 12px; max-height: 70vh; overflow: auto; margin: 0;'
});
this.count = E('span', { 'style': 'color: #888; margin-left: auto;' });
this.filter = E('input', {
'type': 'text',
'class': 'cbi-input-text',
'placeholder': _('Filter…'),
'style': 'max-width: 16em;',
'input': function() { self.show(); }
});
this.levelSel = E('select', { 'class': 'cbi-input-select', 'change': function() { self.show(); } }, [
E('option', { 'value': 'all' }, _('All levels')),
E('option', { 'value': 'warn' }, _('Warnings and errors')),
E('option', { 'value': 'error' }, _('Errors only'))
]);
this.pauseBtn = E('button', {
'class': 'cbi-button',
'click': ui.createHandlerFn(this, 'handlePause')
}, _('Pause'));
poll.add(function() { return self.refresh(); }, 5);
if (typeof requestAnimationFrame === 'function')
requestAnimationFrame(function() { self.show(); });
var node = E('div', { 'class': 'cbi-map' }, [
E('h2', {}, _('meow Log')),
E('div', { 'class': 'cbi-map-descr' },
_('meow entries from the system log, oldest first; refreshes every 5 seconds. ' +
'Errors are shown in red and warnings in orange.')),
E('div', { 'style': 'display: flex; flex-wrap: wrap; gap: .5em; align-items: center; margin-bottom: .5em;' }, [
this.filter, this.levelSel, this.pauseBtn,
E('button', {
'class': 'cbi-button cbi-button-action',
'click': ui.createHandlerFn(this, 'handleDownload')
}, _('Download')),
this.count
]),
E('div', { 'class': 'cbi-section' }, [ this.pre ])
]);
this.show();
return node;
},
handleSave: null,
handleSaveApply: null,
handleReset: null
});
@@ -0,0 +1,255 @@
'use strict';
'require view';
'require dom';
'require fs';
'require poll';
'require ui';
'require uci';
'require tools.meow as meow';
// Status page: service state from procd, everything else from the meow REST
// API (/version, /configs, /connections). Proxy selection, rules, groups and
// subscriptions live in the built-in panel (Panel tab).
var MODES = [ 'rule', 'global', 'direct' ];
var MODE_HELP = {
rule: _('Traffic follows your rules (recommended).'),
global: _('All traffic goes through the proxy selected in the GLOBAL group.'),
direct: _('All traffic bypasses the proxies.')
};
var COLORS = { ok: '#2e7d32', warn: '#ef6c00', bad: '#c62828' };
function badge(kind, text) {
return E('span', {
'class': 'label',
'style': 'color: #fff; padding: 2px 8px; border-radius: 3px; background: ' + COLORS[kind]
}, text);
}
function row(label, value) {
return E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td left', 'width': '33%' }, label),
E('td', { 'class': 'td left' }, value)
]);
}
function tabLink(tab, text) {
return E('a', { 'href': L.url('admin/services/meow', tab) }, text);
}
return view.extend({
load: function() {
return uci.load('meow');
},
gatewayActive: function() {
return fs.exec('/usr/share/meow/gateway.sh', [ 'status' ])
.then(function(res) { return res.code === 0; })
.catch(function() { return false; });
},
// First-run and failure guidance above the status table.
renderBanner: function(enabled, running) {
if (running)
return '';
if (!enabled)
return E('div', { 'class': 'alert-message notice' }, [
E('strong', {}, _('meow is not enabled.')), ' ',
_('Add your proxies in the Configuration tab (or import a subscription in the Panel), then enable the service. '),
E('button', {
'class': 'cbi-button cbi-button-positive',
'click': ui.createHandlerFn(this, 'handleEnable')
}, _('Enable and start'))
]);
return E('div', { 'class': 'alert-message warning' }, [
E('strong', {}, _('meow is stopped.')), ' ',
_('If you did not stop it, the configuration probably failed to load; the reason is in the '),
tabLink('log', _('Log')), '.'
]);
},
refresh: function(nodes) {
var self = this;
var enabled = uci.get('meow', 'main', 'enabled') === '1';
return Promise.all([
meow.serviceRunning(),
L.resolveDefault(meow.api('GET', '/version'), null),
L.resolveDefault(meow.api('GET', '/configs'), null),
L.resolveDefault(meow.api('GET', '/connections'), null),
this.gatewayActive()
]).then(function(r) {
var running = r[0], version = r[1], cfg = r[2], conns = r[3],
gateway = r[4], now = Date.now();
dom.content(nodes.banner, self.renderBanner(enabled, running));
dom.content(nodes.service, running
? [ badge('ok', _('Running')), ' ', version ? 'v' + String(version.version).replace(/^v/, '') : '' ]
: badge(enabled ? 'bad' : 'warn', enabled ? _('Not running') : _('Disabled')));
var tproxy = uci.get('meow', 'tproxy', 'enabled') === '1';
dom.content(nodes.gateway, tproxy
? (gateway
? badge('ok', _('Active'))
: [ badge('warn', _('Waiting')), ' ',
E('small', {}, _('Rules load once meow is listening.')) ])
: [ E('em', {}, _('Off')), ' — ', tabLink('settings', _('enable in Settings')) ]);
var mode = cfg && cfg.mode;
nodes.modeButtons.forEach(function(btn) {
var active = mode === btn.getAttribute('data-mode');
btn.classList.toggle('cbi-button-positive', active);
btn.disabled = !cfg;
});
dom.content(nodes.modeHelp, MODE_HELP[mode] || (running ? '' : _('Available while meow is running.')));
nodes.actions.start.style.display = running ? 'none' : '';
nodes.actions.stop.style.display = running ? '' : 'none';
nodes.actions.restart.style.display = running ? '' : 'none';
if (conns) {
var up = conns.uploadTotal || 0, down = conns.downloadTotal || 0;
if (self.last) {
var dt = (now - self.last.t) / 1000;
dom.content(nodes.rate, '↑ %s/s ↓ %s/s'.format(
meow.formatBytes(Math.max(0, up - self.last.up) / dt),
meow.formatBytes(Math.max(0, down - self.last.down) / dt)));
}
self.last = { t: now, up: up, down: down };
dom.content(nodes.totals, '↑ %s ↓ %s'.format(meow.formatBytes(up), meow.formatBytes(down)));
dom.content(nodes.conns, String((conns.connections || []).length));
} else {
self.last = null;
dom.content(nodes.rate, '-');
dom.content(nodes.totals, '-');
dom.content(nodes.conns, '-');
}
});
},
handleMode: function(mode) {
return meow.api('PATCH', '/configs', { mode: mode }).then(L.bind(function() {
return this.refresh(this.nodes);
}, this)).catch(function(e) {
ui.addNotification(null, E('p', _('Failed to switch mode: %s').format(e.message)), 'error');
});
},
// Runs an init action, then waits briefly for procd to settle and reports
// the resulting state instead of assuming success.
service: function(action) {
var self = this;
var want = action !== 'stop';
return fs.exec('/etc/init.d/meow', [ action ]).then(function(res) {
if (res.code !== 0)
throw new Error(res.stderr || _('Command failed'));
return new Promise(function(resolve) { setTimeout(resolve, 1500); });
}).then(function() {
return meow.serviceRunning();
}).then(function(running) {
if (running !== want)
throw new Error(want ? _('meow did not start; see the Log tab.') : _('meow is still running.'));
ui.addTimeLimitedNotification(null, E('p', {
start: _('meow started.'), stop: _('meow stopped.'), restart: _('meow restarted.')
}[action]), 3000, 'info');
}).catch(function(e) {
ui.addNotification(null, E('p', e.message), 'error');
}).then(function() {
return self.refresh(self.nodes);
});
},
handleStart: function() { return this.service('start'); },
handleStop: function() { return this.service('stop'); },
handleRestart: function() { return this.service('restart'); },
handleEnable: function() {
var self = this;
uci.set('meow', 'main', 'enabled', '1');
return uci.save()
.then(function() { return uci.apply(); })
.then(function() { return self.service('start'); })
.catch(function(e) {
ui.addNotification(null, E('p', _('Failed to enable: %s').format(e.message)), 'error');
});
},
render: function() {
var self = this;
function action(name, cls, text) {
return E('button', {
'class': 'cbi-button ' + cls,
'style': 'display: none;',
'click': ui.createHandlerFn(self, name)
}, text);
}
var nodes = this.nodes = {
banner: E('div'),
service: E('span', {}, _('Collecting data…')),
gateway: E('span', {}, '-'),
modeHelp: E('div', { 'style': 'margin-top: .4em; color: #888;' }),
rate: E('span', {}, '-'),
totals: E('span', {}, '-'),
conns: E('span', {}, '-'),
modeButtons: MODES.map(function(mode) {
return E('button', {
'class': 'cbi-button',
'data-mode': mode,
'disabled': true,
'click': ui.createHandlerFn(self, 'handleMode', mode)
}, mode.charAt(0).toUpperCase() + mode.slice(1));
}),
actions: {
start: action('handleStart', 'cbi-button-positive', _('Start')),
stop: action('handleStop', 'cbi-button-negative', _('Stop')),
restart: action('handleRestart', 'cbi-button-apply', _('Restart'))
}
};
poll.add(function() { return self.refresh(nodes); }, 3);
return E('div', { 'class': 'cbi-map' }, [
E('h2', {}, _('meow')),
E('div', { 'class': 'cbi-map-descr' },
_('Rule-based proxy, compatible with mihomo (Clash Meta) configurations. ' +
'Pick proxies, manage subscriptions and inspect connections in the Panel tab.')),
nodes.banner,
E('div', { 'class': 'cbi-section' }, [
E('table', { 'class': 'table' }, [
row(_('Service'), nodes.service),
row(_('Transparent proxy'), nodes.gateway),
row(_('Proxy mode'), E('div', {}, [
E('div', { 'class': 'cbi-page-actions', 'style': 'text-align: left; padding: 0;' },
nodes.modeButtons),
nodes.modeHelp
])),
row(_('Current speed'), nodes.rate),
row(_('Total traffic'), nodes.totals),
row(_('Active connections'), nodes.conns)
])
]),
E('div', { 'class': 'cbi-page-actions' }, [
// Without a secret the API is loopback-only; the Panel tab explains
// that and offers to set one.
meow.secret()
? E('a', {
'class': 'cbi-button cbi-button-action',
'href': meow.panelURL(),
'target': '_blank',
'rel': 'noopener'
}, _('Open panel'))
: E('a', {
'class': 'cbi-button cbi-button-action',
'href': L.url('admin/services/meow/panel')
}, _('Open panel')),
' ', nodes.actions.start, ' ', nodes.actions.stop, ' ', nodes.actions.restart
])
]);
},
handleSave: null,
handleSaveApply: null,
handleReset: null
});
@@ -0,0 +1,95 @@
'use strict';
'require view';
'require ui';
'require uci';
'require tools.meow as meow';
// Embeds meow's built-in web panel (served by the meow REST API at /ui)
// instead of reimplementing a dashboard in LuCI. The API secret, if any, is
// handed over in the URL fragment so the panel works without re-entering it.
//
// Without a secret the init script binds the API to 127.0.0.1 only, so the
// browser cannot reach it: explain that and offer to set a password instead
// of embedding a frame that can only fail.
function randomSecret() {
var bytes = new Uint8Array(32);
window.crypto.getRandomValues(bytes);
return Array.prototype.map.call(bytes, function(b) {
return ('0' + b.toString(16)).slice(-2);
}).join('');
}
return view.extend({
load: function() {
return uci.load('meow');
},
// Setting the secret makes meow listen on the LAN (with authentication);
// applying the UCI change restarts the service through its reload trigger.
handleEnableLan: function() {
uci.set('meow', 'main', 'secret', randomSecret());
return uci.save()
.then(function() { return uci.apply(); })
.then(function() { return new Promise(function(r) { setTimeout(r, 3000); }); })
.then(function() { location.reload(); })
.catch(function(e) {
ui.addNotification(null, E('p', _('Failed to set the panel password: %s').format(e.message)), 'error');
});
},
renderLocalOnly: function() {
return E('div', { 'class': 'alert-message notice' }, [
E('p', {}, [
E('strong', {}, _('The panel is only reachable from the router itself.')), ' ',
_('No panel password (API secret) is set, so meow keeps its web panel off the LAN. ' +
'Set a password to open the panel from this browser; LuCI passes it to the panel automatically.')
]),
E('p', {}, [
E('button', {
'class': 'cbi-button cbi-button-positive',
'click': ui.createHandlerFn(this, 'handleEnableLan')
}, _('Generate a password and enable the panel')),
' ',
E('a', { 'href': L.url('admin/services/meow/settings') }, _('or choose one in Settings')),
' ',
E('small', {}, _('(meow restarts briefly)'))
])
]);
},
render: function() {
var url = meow.panelURL();
var plain = meow.apiBase() + '/ui';
var body;
if (!meow.secret())
body = this.renderLocalOnly();
else if (window.location.protocol === 'https:')
body = E('p', {}, _('Open the panel in a new tab. The standalone panel uses HTTP and cannot be embedded in an HTTPS page.'));
else
body = E('iframe', {
'src': url,
'style': 'width: 100%; min-height: 75vh; border: none;' +
' border-radius: 3px; background: #0f1923;'
});
return E('div', { 'class': 'cbi-map' }, [
E('h2', {}, _('meow Panel')),
E('div', { 'class': 'cbi-map-descr' }, meow.secret() ? [
_('Built-in web panel served by the meow REST API. '),
E('a', { 'href': url, 'target': '_blank', 'rel': 'noopener' },
_('Open in a new tab')),
' — ', plain,
E('br'),
E('small', {}, _('Blank frame? meow may be stopped (see Overview), or a firewall blocks port %s from your device.')
.format(uci.get('meow', 'main', 'panel_port') || '9090'))
] : _('Built-in web panel served by the meow REST API.')),
body
]);
},
handleSave: null,
handleSaveApply: null,
handleReset: null
});
@@ -0,0 +1,158 @@
'use strict';
'require view';
'require form';
'require dom';
'require fs';
'require tools.meow_settings as settings';
'require poll';
'require uci';
'require tools.meow as meow';
function renderStatus(running) {
return running
? E('span', { 'style': 'color: #2e7d32; font-weight: bold;' },
_('RUNNING'))
: E('span', { 'style': 'color: #c62828; font-weight: bold;' },
_('NOT RUNNING'));
}
return view.extend({
load: function() {
return Promise.all([ uci.load('meow'), uci.load('network') ]);
},
render: function() {
var m, s, o;
m = new form.Map('meow', _('meow'),
_('Service and transparent-proxy settings. Saving also updates the YAML ' +
'configuration to match; Save & Apply restarts meow. Proxies, rules and ' +
'upstream DNS are edited in the Configuration tab.'));
var save = m.save;
m.save = function(callback, silent) {
var rollback;
return save.call(this, function() {
return Promise.resolve().then(callback).then(function() {
return settings.save();
}).then(function(undo) { rollback = undo; });
}, silent).catch(function(error) {
return Promise.resolve(rollback && rollback()).then(function() { throw error; });
});
};
s = m.section(form.NamedSection, 'main', 'meow', _('Service'));
s.tab('basic', _('Basic'));
s.tab('advanced', _('Advanced'));
o = s.taboption('basic', form.DummyValue, '_status', _('Status'));
o.rawhtml = true;
o.cfgvalue = function() {
var node = E('span', {}, _('Collecting data…'));
poll.add(function() {
return meow.serviceRunning().then(function(running) {
dom.content(node, renderStatus(running));
});
});
return node;
};
o = s.taboption('basic', form.Flag, 'enabled', _('Enable meow'),
_('Start meow now and at every boot. Save & Apply restarts it.'));
o.rmempty = false;
o = s.taboption('basic', form.Value, 'panel_port', _('Web panel port'),
_('Port of the built-in web panel and REST API (external-controller in the YAML).'));
o.datatype = 'port';
o.default = '9090';
o.rmempty = false;
o = s.taboption('basic', form.Value, 'secret', _('Panel password'),
_('The API secret. Leave empty to allow the panel only from this router; ' +
'set it to open the panel from other LAN devices.'));
o.password = true;
o = s.taboption('advanced', form.Value, 'config_file', _('Configuration file'),
_('Fixed path used by the managed service; edit it in the Configuration tab.'));
o.default = '/etc/meow/config.yaml';
o.readonly = true;
o.rmempty = false;
o = s.taboption('advanced', form.Value, 'work_dir', _('Working directory'),
_('GeoIP databases, caches and downloaded rule sets are kept here.'));
o.default = '/etc/meow';
o.readonly = true;
o.rmempty = false;
s = m.section(form.NamedSection, 'tproxy', 'transparent', _('Transparent proxy'),
_('Proxy LAN devices without configuring each of them: devices that use this ' +
'router as their gateway (and DNS) are proxied automatically. Local and ' +
'private destinations are never proxied. Individual devices can be excluded ' +
'in the Clients tab. The YAML configuration is updated to match on save.'));
s.addremove = false;
s.tab('basic', _('Basic'));
s.tab('advanced', _('Advanced'));
o = s.taboption('basic', form.DummyValue, '_gateway', _('Status'));
o.rawhtml = true;
o.cfgvalue = function() {
var node = E('span', {}, _('Collecting data…'));
poll.add(function() {
return fs.exec('/usr/share/meow/gateway.sh', [ 'status' ]).catch(function() {
return { code: 1 };
}).then(function(res) {
dom.content(node, uci.get('meow', 'tproxy', 'enabled') !== '1'
? E('em', {}, _('Off'))
: res.code === 0
? E('span', { 'style': 'color: #2e7d32; font-weight: bold;' }, _('Active'))
: E('span', { 'style': 'color: #ef6c00; font-weight: bold;' },
_('Not active — waiting for meow (check the Log tab if this persists)')));
});
});
return node;
};
o = s.taboption('basic', form.Flag, 'enabled', _('Proxy LAN devices'));
o.rmempty = false;
o = s.taboption('basic', form.ListValue, 'mode', _('Traffic to proxy'));
o.value('tproxy', _('TCP and UDP (recommended)'));
o.value('redirect', _('TCP only (for kernels without TPROXY support)'));
o.default = 'tproxy';
o = s.taboption('basic', form.ListValue, 'interface', _('LAN interface'),
_('Devices connected through this interface are proxied.'));
uci.sections('network', 'interface', function(sec) {
if (sec['.name'] !== 'loopback')
o.value(sec['.name']);
});
o.default = 'lan';
o = s.taboption('basic', form.Flag, 'dns_hijack', _('Handle LAN DNS'),
_('Answer every DNS query from LAN devices with meow. Required for fake-ip mode.'));
o.default = '1';
o.rmempty = false;
o = s.taboption('advanced', form.Value, 'tproxy_port', _('Listener port'),
_('Port of the transparent-proxy listener created in the YAML configuration.'));
o.datatype = 'port';
o.default = '7893';
o = s.taboption('advanced', form.Value, 'dns_port', _('DNS port'),
_('Port meow\'s DNS server listens on (dns.listen in the YAML).'));
o.datatype = 'port';
o.default = '1053';
o.depends('dns_hijack', '1');
o = s.taboption('advanced', form.Flag, 'ipv6', _('Proxy IPv6'),
_('Also proxy IPv6 TCP. Leave off with fake-ip, which hides IPv6 answers.'));
o.rmempty = false;
o.depends('mode', 'redirect');
o = s.taboption('advanced', form.DynamicList, 'bypass', _('Never proxy these destinations'),
_('Extra networks (CIDR, e.g. 203.0.113.0/24) that always go direct, on top of private and reserved ranges.'));
o.datatype = 'cidr';
return m.render();
}
});
+514
View File
@@ -0,0 +1,514 @@
{
"name": "luci-app-meow-build",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "luci-app-meow-build",
"devDependencies": {
"esbuild": "0.28.2",
"yaml": "2.9.1"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz",
"integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz",
"integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz",
"integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz",
"integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz",
"integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz",
"integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz",
"integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz",
"integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz",
"integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz",
"integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz",
"integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz",
"integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz",
"integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz",
"integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz",
"integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz",
"integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz",
"integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz",
"integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz",
"integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz",
"integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz",
"integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz",
"integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz",
"integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz",
"integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz",
"integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz",
"integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/esbuild": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz",
"integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.28.2",
"@esbuild/android-arm": "0.28.2",
"@esbuild/android-arm64": "0.28.2",
"@esbuild/android-x64": "0.28.2",
"@esbuild/darwin-arm64": "0.28.2",
"@esbuild/darwin-x64": "0.28.2",
"@esbuild/freebsd-arm64": "0.28.2",
"@esbuild/freebsd-x64": "0.28.2",
"@esbuild/linux-arm": "0.28.2",
"@esbuild/linux-arm64": "0.28.2",
"@esbuild/linux-ia32": "0.28.2",
"@esbuild/linux-loong64": "0.28.2",
"@esbuild/linux-mips64el": "0.28.2",
"@esbuild/linux-ppc64": "0.28.2",
"@esbuild/linux-riscv64": "0.28.2",
"@esbuild/linux-s390x": "0.28.2",
"@esbuild/linux-x64": "0.28.2",
"@esbuild/netbsd-arm64": "0.28.2",
"@esbuild/netbsd-x64": "0.28.2",
"@esbuild/openbsd-arm64": "0.28.2",
"@esbuild/openbsd-x64": "0.28.2",
"@esbuild/openharmony-arm64": "0.28.2",
"@esbuild/sunos-x64": "0.28.2",
"@esbuild/win32-arm64": "0.28.2",
"@esbuild/win32-ia32": "0.28.2",
"@esbuild/win32-x64": "0.28.2"
}
},
"node_modules/yaml": {
"version": "2.9.1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz",
"integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==",
"dev": true,
"license": "ISC",
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14.6"
},
"funding": {
"url": "https://github.com/sponsors/eemeli"
}
}
}
}
@@ -0,0 +1,6 @@
{
"name": "luci-app-meow-build",
"private": true,
"scripts": { "build": "node vendor/build.cjs" },
"devDependencies": { "esbuild": "0.28.2", "yaml": "2.9.1" }
}
@@ -0,0 +1,24 @@
#!/bin/sh
# Small, allowlisted bridge from authenticated LuCI RPC to the local HTTP API.
# Caller-controlled hosts, ports and arbitrary API paths are never accepted.
set -eu
case "$#:${1:-}:${2:-}" in
2:GET:/version|2:GET:/configs|2:GET:/connections) body='' ;;
3:PATCH:/configs)
mode=$(jsonfilter -s "$3" -e '@.mode')
case "$mode" in rule|global|direct) ;; *) echo 'Invalid mode' >&2; exit 1 ;; esac
body="{\"mode\":\"$mode\"}"
;;
*) echo 'Unsupported API request' >&2; exit 1 ;;
esac
method=$1
path=$2
port=$(uci -q get meow.main.panel_port || echo 9090)
case "$port" in ''|*[!0-9]*) echo 'Invalid API port' >&2; exit 1 ;; esac
[ "$port" -ge 1 ] && [ "$port" -le 65535 ] || exit 1
secret=$(uci -q get meow.main.secret || true)
set -- --noproxy '*' --silent --show-error --fail --connect-timeout 2 --max-time 5 \
--request "$method" "http://127.0.0.1:$port$path"
[ -z "$secret" ] || set -- "$@" --header "Authorization: Bearer $secret"
[ -z "$body" ] || set -- "$@" --header 'Content-Type: application/json' --data-binary "$body"
exec curl "$@"
@@ -0,0 +1,14 @@
#!/bin/sh
# No caller-supplied CLI flags or paths: only the generated scratch token.
set -eu
[ "$#" -eq 1 ] || exit 1
case "$1" in
check) scratch=/tmp/meow-luci-check.yaml ;;
*)
[ "${#1}" -eq 32 ] || exit 1
case "$1" in *[!0-9a-f]*) exit 1 ;; esac
scratch="/tmp/meow-luci-settings-$1.yaml"
;;
esac
# Delegated LuCI configuration is deliberately confined to /etc/meow.
exec /usr/bin/meow --no-external-plugins -d /etc/meow -f "$scratch" -t
@@ -0,0 +1,67 @@
{
"admin/services/meow": {
"title": "meow",
"order": 60,
"action": {
"type": "firstchild"
},
"depends": {
"acl": [ "luci-app-meow" ],
"uci": { "meow": true }
}
},
"admin/services/meow/overview": {
"title": "Overview",
"order": 10,
"action": {
"type": "view",
"path": "meow/overview"
}
},
"admin/services/meow/panel": {
"title": "Panel",
"order": 20,
"action": {
"type": "view",
"path": "meow/panel"
}
},
"admin/services/meow/config": {
"title": "Configuration",
"order": 30,
"action": {
"type": "view",
"path": "meow/config"
}
},
"admin/services/meow/clients": {
"title": "Clients",
"order": 35,
"action": {
"type": "view",
"path": "meow/clients"
}
},
"admin/services/meow/settings": {
"title": "Settings",
"order": 40,
"action": {
"type": "view",
"path": "meow/settings"
}
},
"admin/services/meow/log": {
"title": "Log",
"order": 50,
"action": {
"type": "view",
"path": "meow/log"
}
}
}
@@ -0,0 +1,43 @@
{
"luci-app-meow": {
"description": "Grant access to meow service configuration",
"read": {
"uci": [ "meow", "network", "dhcp" ],
"ubus": {
"luci-rpc": [ "getHostHints", "getDHCPLeases" ],
"service": [ "list" ],
"file": [ "exec" ],
"uci": [ "get", "changes" ]
},
"file": {
"/sbin/logread -e meow": [ "exec" ],
"/usr/share/meow/gateway.sh status": [ "exec" ],
"/usr/share/meow/arp-hijack.sh clients": [ "exec" ],
"/usr/libexec/meow-api GET /version": [ "exec" ],
"/usr/libexec/meow-api GET /configs": [ "exec" ],
"/usr/libexec/meow-api GET /connections": [ "exec" ],
"/etc/meow/config.yaml": [ "read" ]
},
"cgi-io": [ "download", "exec" ]
},
"write": {
"uci": [ "meow" ],
"file": {
"/tmp/meow-luci-check.yaml": [ "write" ],
"/tmp/meow-luci-settings-*.yaml": [ "write" ],
"/etc/init.d/meow start": [ "exec" ],
"/etc/init.d/meow stop": [ "exec" ],
"/etc/init.d/meow restart": [ "exec" ],
"/etc/init.d/meow-arp restart": [ "exec" ],
"/usr/libexec/meow-api PATCH /configs *": [ "exec" ],
"/etc/meow/config.yaml": [ "write" ],
"/usr/libexec/meow-validate *": [ "exec" ]
},
"ubus": {
"file": [ "exec", "remove" ],
"uci": [ "add", "apply", "confirm", "delete", "order", "rename", "set" ]
},
"cgi-io": [ "upload" ]
}
}
}
+20
View File
@@ -0,0 +1,20 @@
# YAML document parser
`tools/meow_yaml.js` is a browser-only bundle of the ISC-licensed
[yaml](https://eemeli.org/yaml/) document API. It retains comments, aliases and
unrelated configuration while LuCI edits its managed keys. The bundled file
and `meow_yaml.LICENSE` ship in the LuCI package; npm is not needed on OpenWrt.
Regenerate from pinned dependencies:
```sh
cd openwrt/luci-app-meow
npm ci
npm run build
```
The Settings map's save callback runs after LuCI parses form values and before
UCI is saved. YAML validation/upload failures reject that save. If UCI saving
subsequently fails, the callback's rollback restores the previous YAML unless
another writer has changed it. The separate Configuration editor applies the
same transformation before validating imported YAML.
+14
View File
@@ -0,0 +1,14 @@
const { buildSync } = require('esbuild');
const { copyFileSync } = require('node:fs');
const path = require('node:path');
const root = path.join(__dirname, '..');
buildSync({
entryPoints: [path.join(__dirname, 'yaml.js')],
outfile: path.join(root, 'htdocs/luci-static/resources/tools/meow_yaml.js'),
bundle: true, minify: true, platform: 'browser', format: 'iife',
globalName: 'MeowYaml', target: 'es2020',
banner: { js: "'use strict';\n'require baseclass';\n// Generated by npm ci && npm run build in openwrt/luci-app-meow; yaml 2.9.1 (ISC)." },
footer: { js: 'return baseclass.extend(MeowYaml);' }
});
copyFileSync(path.join(root, 'node_modules/yaml/LICENSE'),
path.join(root, 'htdocs/luci-static/resources/tools/meow_yaml.LICENSE'));
+1
View File
@@ -0,0 +1 @@
export { parseDocument, isMap, isSeq } from 'yaml';
+108
View File
@@ -0,0 +1,108 @@
#!/bin/sh
# ARP-based client steering for meow side-router / gateway mode.
#
# For the LAN clients you SELECT, this periodically sends unicast ARP replies
# claiming the network's real gateway IP is at this device's MAC. Those clients
# then send their off-LAN traffic here instead, so meow's transparent proxy
# handles them WITHOUT any per-client configuration and without changing the
# main router's DHCP.
#
# This is ARP spoofing. Use it only to steer devices you own or administer, on
# a network you control. It is OFF by default and touches only the MACs listed
# in the `arp_hijack` section of /etc/config/meow — never the whole LAN.
#
# A client is restored by simply de-selecting it (its ARP cache relearns the
# real gateway within its normal timeout once we stop announcing).
#
# Usage: arp-hijack.sh clients | run | once
# Settings come from the `arp_hijack` section of /etc/config/meow.
. /lib/functions.sh
. /lib/functions/network.sh
clients=""
add_client() { clients="$clients $1"; }
load_config() {
config_load meow
config_get_bool enabled arp enabled 0
config_get interface arp interface lan
config_get gateway arp gateway ''
config_get interval arp interval 2
config_list_foreach arp client add_client
network_get_device device "$interface"
[ -n "$gateway" ] || network_get_gateway gateway "$interface"
}
# JSON array of the LAN neighbour table, marking which MACs are selected.
cmd_clients() {
load_config
local sel
sel=" $(echo "$clients" | tr 'A-F' 'a-f') "
printf '['
[ -n "$device" ] && ip neigh show dev "$device" 2>/dev/null | awk -v sel="$sel" '
{
ip=$1; mac=""; st=$NF
for (i=1;i<=NF;i++) if ($i=="lladdr") mac=$(i+1)
if (mac=="" || index(ip,":")) next
lm=tolower(mac)
s=(index(sel," " lm " ")?"true":"false")
printf "%s{\"ip\":\"%s\",\"mac\":\"%s\",\"state\":\"%s\",\"selected\":%s}", (c++?",":""), ip, mac, st, s
}'
printf ']\n'
}
# Current IP for a selected MAC (resolved fresh each sweep; DHCP may move it).
mac_to_ip() {
ip neigh show dev "$device" 2>/dev/null | awk -v m="$(echo "$1" | tr 'A-F' 'a-f')" '
{ mac=""; for (i=1;i<=NF;i++) if ($i=="lladdr") mac=tolower($(i+1))
if (mac==m && !index($1,":")) { print $1; exit } }'
}
sweep() {
local mac ip
for mac in $clients; do
ip=$(mac_to_ip "$mac")
[ -n "$ip" ] || continue
# ARP reply: "gateway is at <our MAC>", unicast to the client.
if ! /usr/bin/meow arp-reply "$device" "$gateway" "$mac" "$ip"; then
logger -t meow-arp "failed to send unicast reply to $mac ($ip)"
return 1
fi
done
}
precheck() {
case "$interval" in ''|*[!0-9]*) logger -t meow-arp 'interval must be an integer from 1 to 3600'; return 1 ;; esac
[ "$interval" -ge 1 ] && [ "$interval" -le 3600 ] || return 1
[ -n "$device" ] || { logger -t meow-arp "no device for interface '$interface'"; return 1; }
[ -n "$gateway" ] || { logger -t meow-arp "no gateway for '$interface'; set arp.gateway"; return 1; }
return 0
}
case "$1" in
clients)
cmd_clients
;;
once)
load_config
precheck || exit 1
sweep
;;
run)
load_config
[ "$enabled" -eq 1 ] || { logger -t meow-arp "disabled; not steering"; exit 0; }
precheck || exit 1
[ -n "$clients" ] || { logger -t meow-arp "no clients selected; idle"; }
logger -t meow-arp "steering [$clients ] via $device as gateway $gateway"
while :; do
sweep || exit 1
sleep "$interval"
done
;;
*)
echo "usage: $0 clients|run|once" >&2
exit 1
;;
esac
+82
View File
@@ -0,0 +1,82 @@
# Default meow-rs configuration shipped with the OpenWrt package.
# Edit this file, then restart the service: /etc/init.d/meow restart
# Full reference: https://github.com/madeye/meow-rs/blob/main/config.example.yaml
# Combined HTTP + SOCKS5 proxy for LAN clients.
mixed-port: 7890
allow-lan: true
bind-address: "0.0.0.0"
mode: rule
log-level: info
ipv6: false
# REST API + built-in web panel (served at /ui). The init script overrides
# the address with `--ext-ctl 0.0.0.0:<panel_port>` and, when set, the secret
# with `--secret <secret>` from /etc/config/meow, so LuCI always knows how to
# reach it. OpenWrt's default firewall still blocks WAN-side access.
external-controller: 0.0.0.0:9090
secret: ""
# SO_MARK for DIRECT sockets (loop avoidance on Linux).
routing-mark: 9527
# Transparent-proxy listener for forwarded LAN traffic (gateway / side-router
# mode, enabled in LuCI or via `uci set meow.tproxy.enabled=1`). Must bind a
# non-loopback IPv4 address; the port must match `tproxy_port` in
# /etc/config/meow.
#
# `firewall: false` leaves all rule management to gateway.sh (this package's
# nftables rules) — meow makes no nft calls itself. `udp: true` enables UDP
# TPROXY, which requires `firewall: false`, an IPv4 listen, and the
# deployer-owned fwmark + policy routing that gateway.sh's `tproxy` mode sets
# up. In `redirect` mode gateway.sh uses nat REDIRECT and only TCP is proxied;
# the same listener accepts it (UDP is simply never delivered).
listeners:
- name: tproxy-lan
type: tproxy
listen: 0.0.0.0
port: 7893
udp: true
firewall: false
# A gateway serves many concurrent flows from every LAN client; the 256
# default saturates under a few busy devices and stalls new connections.
# 0 = unlimited.
max-connections: 4096
# Recover hostnames for connections made to raw IPs.
sniffer:
enable: true
override-destination: false
sniff:
TLS: { ports: [443] }
HTTP: { ports: [80] }
dns:
enable: true
# dnsmasq owns :53; meow's resolver listens on 1053. LAN DNS is redirected
# here when transparent proxy + DNS hijack are enabled (`dns_port`).
listen: 0.0.0.0:1053
enhanced-mode: fake-ip
fake-ip-range: 198.18.0.0/16
nameserver:
- 223.5.5.5
- 8.8.8.8
# Add your proxies and groups here, e.g.:
# proxies:
# - name: "ss-example"
# type: ss
# server: 1.2.3.4
# port: 8388
# cipher: aes-256-gcm
# password: "your-password"
# udp: true
#
# proxy-groups:
# - name: "PROXY"
# type: select
# proxies: ["ss-example", "DIRECT"]
rules:
- MATCH,DIRECT
+234
View File
@@ -0,0 +1,234 @@
#!/bin/sh
# Transparent-proxy rules for traffic forwarded through this router
# (gateway or side-router mode). meow's built-in `inet meow_tproxy` table only
# covers the router's own output traffic; this adds `inet meow_gateway`, which
# hooks prerouting on the LAN device. See docs/tproxy-gateway.md.
#
# Modes (`option mode`):
# tproxy REDIRECT for TCP + kernel TPROXY for UDP (default). meow's TCP
# listener recovers the original destination via SO_ORIGINAL_DST,
# which only nat REDIRECT populates, so TCP is never sent through
# kernel TPROXY; UDP has a real IP_TRANSPARENT listener.
# redirect nat REDIRECT, TCP only
#
# Usage: gateway.sh up|down|wait|status
# Settings come from the `tproxy` section of /etc/config/meow.
. /lib/functions.sh
. /lib/functions/network.sh
TABLE="inet meow_gateway"
RULES=/var/run/meow/gateway.nft
# Two marks, deliberately different (see docs/tproxy-gateway.md):
# FWMARK tags the UDP datagrams the mangle chain hands to the TPROXY
# listener; a policy route delivers those marked packets
# locally. TCP never uses it.
# ROUTING_MARK meow's own outbound sockets carry this (SO_MARK, from the
# YAML `routing-mark`), so both capture chains RETURN on it and
# meow's own traffic is never re-proxied into a loop. This MUST
# equal `routing-mark` in the meow YAML config (default 9527).
FWMARK=0x2333
ROUTING_MARK=0x2537
ROUTE_TABLE=233
load_config() {
config_load meow
config_get interface tproxy interface lan
config_get mode tproxy mode tproxy
config_get tproxy_port tproxy tproxy_port 7893
config_get_bool dns_hijack tproxy dns_hijack 1
config_get dns_port tproxy dns_port 1053
config_get_bool ipv6 tproxy ipv6 0
config_get bypass tproxy bypass ''
config_get bypass_mac tproxy bypass_mac ''
bypass_mac=$(echo "$bypass_mac" | tr 'A-F' 'a-f')
}
validate_config() {
case "$mode:$ipv6" in redirect:0|redirect:1|tproxy:0) ;; *)
logger -t meow 'gateway: invalid mode/IPv6 combination; IPv6 requires redirect mode'; return 1 ;; esac
for port in "$tproxy_port" "$dns_port"; do
case "$port" in ''|*[!0-9]*) return 1 ;; esac
[ "$port" -ge 1 ] && [ "$port" -le 65535 ] || return 1
done
case "$interface" in ''|*[!a-zA-Z0-9_.:-]*) return 1 ;; esac
for cidr in $bypass; do
case "$cidr" in *[!0-9a-fA-F.:/]*|'') return 1 ;; esac
done
local h='[0-9a-f][0-9a-f]'
for mac in $bypass_mac; do
case "$mac" in $h:$h:$h:$h:$h:$h) ;; *) return 1 ;; esac
done
}
port_listening() {
# /proc/net/tcp{,6} list local ports in hex; state 0A is LISTEN.
local hex
hex=$(printf '%04X' "$1")
grep -qE "^ *[0-9]+: [0-9A-F]+:$hex [0-9A-F]+:[0-9A-F]+ 0A" \
/proc/net/tcp /proc/net/tcp6 2>/dev/null
}
gen_rules() {
local device="$1" extra4="" extra6="" macs="" cidr mac
for cidr in $bypass; do
case "$cidr" in
*:*) extra6="$extra6, $cidr" ;;
*) extra4="$extra4, $cidr" ;;
esac
done
for mac in $bypass_mac; do
macs="${macs:+$macs, }$mac"
done
# TCP goes through nat REDIRECT in BOTH modes (dstnat chain): meow's TCP
# listener recovers the original destination with SO_ORIGINAL_DST, which
# needs the conntrack DNAT that REDIRECT creates. Kernel TPROXY leaves no
# conntrack entry, so a TPROXY'd TCP flow resolves to the listener's own
# address and meow dials itself — a saturating loop. UDP has no REDIRECT
# equivalent and a real IP_TRANSPARENT listener, so it uses TPROXY.
cat <<-NFT
table inet meow_gateway {
set reserved4 {
type ipv4_addr; flags interval; auto-merge
elements = {
0.0.0.0/8, 10.0.0.0/8, 100.64.0.0/10, 127.0.0.0/8,
169.254.0.0/16, 172.16.0.0/12, 192.168.0.0/16,
224.0.0.0/4, 240.0.0.0/4$extra4
}
}
set reserved6 {
type ipv6_addr; flags interval; auto-merge
elements = { ::/128, ::1/128, fc00::/7, fe80::/10, ff00::/8$extra6 }
}
# LAN clients (by MAC) that skip both capture and the DNS hijack.
set bypass_src {
type ether_addr
NFT
[ -n "$macs" ] && echo " elements = { $macs }"
cat <<-NFT
}
chain dstnat {
type nat hook prerouting priority dstnat - 5; policy accept;
iifname != "$device" return
meta mark $ROUTING_MARK return
ether saddr @bypass_src return
NFT
[ "$dns_hijack" -eq 1 ] &&
echo " meta nfproto ipv4 meta l4proto udp th dport 53 redirect to :$dns_port"
[ "$ipv6" -eq 1 ] || echo " meta nfproto ipv6 return"
cat <<-NFT
fib daddr type local return
ip daddr @reserved4 return
ip6 daddr @reserved6 return
meta l4proto tcp redirect to :$tproxy_port
}
NFT
# redirect mode stops at TCP; tproxy mode adds the UDP TPROXY chain.
if [ "$mode" != redirect ]; then
cat <<-NFT
chain mangle_tproxy {
type filter hook prerouting priority mangle; policy accept;
iifname != "$device" return
meta mark $ROUTING_MARK return
ether saddr @bypass_src return
NFT
[ "$dns_hijack" -eq 1 ] && echo " meta l4proto udp th dport 53 return"
[ "$ipv6" -eq 1 ] || echo " meta nfproto ipv6 return"
cat <<-NFT
fib daddr type local return
ip daddr @reserved4 return
ip6 daddr @reserved6 return
meta l4proto udp socket transparent 1 meta mark set $FWMARK accept
meta l4proto udp tproxy to :$tproxy_port meta mark set $FWMARK accept
}
NFT
fi
echo "}"
}
route_up() {
ip rule add fwmark $FWMARK lookup $ROUTE_TABLE || return 1
ip route replace local 0.0.0.0/0 dev lo table $ROUTE_TABLE || return 1
if [ "$ipv6" -eq 1 ]; then
ip -6 rule add fwmark $FWMARK lookup $ROUTE_TABLE
ip -6 route replace local ::/0 dev lo table $ROUTE_TABLE
fi
}
route_down() {
while ip rule del fwmark $FWMARK lookup $ROUTE_TABLE 2>/dev/null; do :; done
ip route flush table $ROUTE_TABLE 2>/dev/null
while ip -6 rule del fwmark $FWMARK lookup $ROUTE_TABLE 2>/dev/null; do :; done
ip -6 route flush table $ROUTE_TABLE 2>/dev/null
return 0
}
case "$1" in
run)
child=''
trap '[ -z "$child" ] || kill "$child" 2>/dev/null; "$0" down; exit 0' TERM INT
"$0" wait & child=$!
wait "$child" || exit 1
child=''
"$0" up || exit 1
while :; do sleep 3600 & child=$!; wait "$child"; done
;;
up)
load_config
validate_config || { logger -t meow "gateway: invalid configuration"; exit 1; }
network_get_device device "$interface"
[ -n "$device" ] || {
logger -t meow "gateway: no device for interface '$interface'"
exit 1
}
case "$device" in *[!a-zA-Z0-9_.:-]*) exit 1 ;; esac
mkdir -p "${RULES%/*}"
gen_rules "$device" > "$RULES"
if ! nft -c -f "$RULES"; then
logger -t meow 'gateway: invalid rules or missing kernel support; install kmod-nft-socket kmod-nft-tproxy (opkg install, or apk add on newer OpenWrt)'
exit 1
fi
nft delete table $TABLE 2>/dev/null
route_down
if ! nft -f "$RULES"; then
logger -t meow "gateway: failed to load $RULES"
exit 1
fi
if [ "$mode" = redirect ]; then
logger -t meow "gateway: redirect $device tcp -> :$tproxy_port"
else
route_up || { "$0" down; logger -t meow "gateway: policy routing failed"; exit 1; }
logger -t meow "gateway: tproxy $device tcp(redirect)+udp -> :$tproxy_port"
fi
;;
down)
nft delete table $TABLE 2>/dev/null
route_down
;;
wait)
# Block until meow binds the tproxy listener (30s cap, then load
# anyway so the rules fail closed rather than leaking traffic).
load_config
i=0
while [ $i -lt 150 ]; do
port_listening "$tproxy_port" && exit 0
sleep 0.2
i=$((i + 1))
done
logger -t meow "gateway: :$tproxy_port not listening after 30s"
exit 0
;;
status)
nft list table $TABLE
;;
*)
echo "usage: $0 up|down|wait|status" >&2
exit 1
;;
esac
+29
View File
@@ -0,0 +1,29 @@
#!/bin/sh /etc/rc.common
# procd init for meow's ARP client-steering loop (arp-hijack.sh). Self-gates on
# the `arp_hijack` section of /etc/config/meow: does nothing unless enabled and
# at least the feature is turned on. Separate from the meow service so steering
# can be toggled without restarting the proxy.
USE_PROCD=1
START=96
STOP=9
PROG=/usr/share/meow/arp-hijack.sh
start_service() {
local enabled
config_load meow
config_get_bool enabled arp enabled 0
[ "$enabled" -eq 1 ] || return 0
procd_open_instance meow-arp
procd_set_param command "$PROG" run
procd_set_param respawn 3600 5 5
procd_set_param stdout 1
procd_set_param stderr 1
procd_close_instance
}
service_triggers() {
procd_add_reload_trigger "meow"
}
+56
View File
@@ -0,0 +1,56 @@
config meow 'main'
# Set to '1' to start meow at boot and via /etc/init.d/meow start.
option enabled '0'
# meow YAML configuration (proxies, rules, DNS, listeners, ...).
option config_file '/etc/meow/config.yaml'
# Working directory for GeoIP databases, caches, downloaded rulesets.
option work_dir '/etc/meow'
# Port of the REST API / built-in web panel. Passed to meow as
# `--ext-ctl 0.0.0.0:<port>`, overriding `external-controller` in the YAML.
option panel_port '9090'
# A random API secret is generated on install. Empty binds to loopback only.
# When non-empty it is passed as `--secret`, overriding
# `secret:` in the YAML; LuCI uses it to talk to the API and the panel.
option secret ''
# Transparent proxy for forwarded LAN traffic (gateway / side-router mode).
# Loads an nftables table `inet meow_gateway` that hands traffic arriving on
# the LAN interface to meow's tproxy listener and hijacks DNS to meow's
# resolver. The YAML config must declare a matching non-loopback tproxy
# listener and DNS listener (the shipped default config does).
config transparent 'tproxy'
option enabled '0'
# 'tproxy': REDIRECT for TCP + kernel TPROXY for UDP. 'redirect': TCP only.
option mode 'tproxy'
# Logical OpenWrt interface whose clients are proxied.
option interface 'lan'
# Port of the `type: tproxy` listener in the YAML config.
option tproxy_port '7893'
# Redirect LAN DNS (port 53, any resolver) to meow's DNS listener.
option dns_hijack '1'
# Port of meow's DNS listener (`dns.listen` in the YAML config).
option dns_port '1053'
# Also proxy IPv6. Off by default: fake-ip suppresses AAAA.
option ipv6 '0'
# Extra destination CIDRs that bypass the proxy (besides reserved ranges).
# list bypass '203.0.113.0/24'
# LAN client MACs whose traffic bypasses the proxy entirely (no capture, no
# DNS hijack). Managed from LuCI (Services -> meow -> Clients).
# list bypass_mac 'aa:bb:cc:dd:ee:ff'
# ARP-based client steering (side-router / gateway mode). For SELECTED clients,
# periodically announce this device as the LAN gateway so their traffic is
# transparently proxied without per-client config. This is ARP spoofing: use it
# only for devices you administer on a network you control. OFF by default; only
# the MACs listed here are ever touched. Managed from LuCI (Services -> meow ->
# Clients). Uses meow's built-in unicast ARP sender.
config arp_hijack 'arp'
option enabled '0'
# LAN interface whose clients are steered.
option interface 'lan'
# Gateway IP to impersonate. Empty = this interface's real gateway.
option gateway ''
# Seconds between ARP announcements (caches expire, so we re-announce).
option interval '2'
# Selected client MACs (one per line). Managed from the Clients tab.
# list client 'aa:bb:cc:dd:ee:ff'
+73
View File
@@ -0,0 +1,73 @@
#!/bin/sh /etc/rc.common
# procd init script for meow-rs.
# Service settings live in /etc/config/meow (UCI); the proxy configuration
# itself is the regular meow YAML file (default /etc/meow/config.yaml).
USE_PROCD=1
START=95
STOP=10
PROG=/usr/bin/meow
GATEWAY=/usr/share/meow/gateway.sh
start_service() {
local enabled config_file work_dir panel_port secret tproxy controller
config_load meow
config_get_bool enabled main enabled 0
config_get config_file main config_file /etc/meow/config.yaml
config_get work_dir main work_dir /etc/meow
config_get panel_port main panel_port 9090
config_get secret main secret ''
config_get_bool tproxy tproxy enabled 0
[ "$enabled" -eq 1 ] || return 0
[ "$config_file" = /etc/meow/config.yaml ] && [ "$work_dir" = /etc/meow ] || {
logger -t meow 'managed service requires /etc/meow/config.yaml and work_dir /etc/meow'
return 1
}
if ! "$PROG" --no-external-plugins -d "$work_dir" -f "$config_file" -t >/dev/null 2>&1; then
logger -t meow "config test failed for $config_file, not starting"
return 1
fi
controller="127.0.0.1:$panel_port"
[ -z "$secret" ] || controller="0.0.0.0:$panel_port"
procd_open_instance meow
procd_set_param command "$PROG" --no-external-plugins -d "$work_dir" -f "$config_file" \
--ext-ctl "$controller"
[ -n "$secret" ] && procd_append_param command --secret "$secret"
# Restart when either the UCI service settings or the YAML config change.
procd_set_param file /etc/config/meow "$config_file"
procd_set_param respawn 3600 5 5
procd_set_param stdout 1
procd_set_param stderr 1
procd_set_param limits nofile="1000000 1000000"
procd_close_instance
if [ "$tproxy" -eq 1 ]; then
# A supervised instance owns the wait and rules, including teardown.
procd_open_instance gateway
procd_set_param command "$GATEWAY" run
procd_set_param term_timeout 5
procd_set_param stdout 1
procd_set_param stderr 1
procd_close_instance
else
"$GATEWAY" down
fi
}
stop_service() {
"$GATEWAY" down
}
service_triggers() {
procd_add_reload_trigger "meow"
}
reload_service() {
stop
start
}
@@ -0,0 +1,30 @@
#!/bin/sh
# Add sections introduced after the first release to preserved configs.
# Preserve existing credentials on upgrades; initialize an empty install.
if [ -z "$(uci -q get meow.main.secret)" ]; then
secret=$(head -c 32 /dev/urandom | hexdump -v -e '1/1 "%02x"')
[ "${#secret}" -eq 64 ] || exit 1
uci set "meow.main.secret=$secret"
fi
if ! uci -q get meow.tproxy >/dev/null; then
uci -q batch <<-UCI
set meow.tproxy=transparent
set meow.tproxy.enabled='0'
set meow.tproxy.mode='tproxy'
set meow.tproxy.interface='lan'
set meow.tproxy.tproxy_port='7893'
set meow.tproxy.dns_hijack='1'
set meow.tproxy.dns_port='1053'
set meow.tproxy.ipv6='0'
UCI
fi
if ! uci -q get meow.arp >/dev/null; then
uci -q batch <<-UCI
set meow.arp=arp_hijack
set meow.arp.enabled='0'
set meow.arp.interface='lan'
set meow.arp.interval='2'
UCI
fi
uci -q commit meow
exit 0