mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-05 18:44:03 +08:00
🤞 Sync 2026-09-29 21:07:16
This commit is contained in:
1 parent
52b5905a85
commit
08343f89fa
231 files changed
+179633
-1348
No files matched your search
@@ -0,0 +1 @@
|
||||
c7fb12c3fac601feb975ed05c3fca594b0877e48
|
||||
@@ -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/`。
|
||||
@@ -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
|
||||
@@ -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).
|
||||
Executable
+200
@@ -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
|
||||
Executable
+235
@@ -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
@@ -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
@@ -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
@@ -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'));
|
||||
@@ -0,0 +1 @@
|
||||
export { parseDocument, isMap, isSeq } from 'yaml';
|
||||
@@ -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
|
||||
@@ -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
|
||||
Executable
+234
@@ -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
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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'
|
||||
Executable
+73
@@ -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
|
||||
Reference in new issue
Block a user