@celilo/e2e 0.5.1 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/bin/cele2e +14 -0
- package/bin/e2e-cache-images +57 -0
- package/bin/e2e-shell +36 -13
- package/bin/e2e-status +8 -2
- package/bin/e2e-up +5 -5
- package/completions/install-zsh-completion.sh +78 -0
- package/config/dns/celilo.computer.zone +16 -0
- package/config/dns/iamtheinternet.org.zone +7 -6
- package/config/dns/knot-namecheap.conf +8 -0
- package/config/dns/knot-tld.conf +4 -0
- package/config/dns/lunacycle.net.zone +16 -0
- package/config/dns/net.zone +11 -0
- package/config/dns/org.zone +5 -0
- package/config/dns/root.zone +2 -0
- package/config/dns/templates/iamtheinternet.org.zone +16 -0
- package/config/dns/templates/lunacycle.net.zone +16 -0
- package/config/resolver/unbound.conf +10 -0
- package/config/routing/docker-image-preload.service +14 -0
- package/config/routing/docker-image-preload.sh +24 -0
- package/config/routing/management-routes.sh +24 -2
- package/config/routing/resolver-internal-routes.sh +59 -0
- package/docker/Dockerfile.internal-resolver +16 -0
- package/docker/Dockerfile.isitup +37 -0
- package/docker/Dockerfile.management +2 -0
- package/docker/Dockerfile.registry +34 -0
- package/docker/Dockerfile.target-machine +20 -0
- package/docker/Dockerfile.target-machine-docker +11 -19
- package/docker/Dockerfile.target-technitium +97 -0
- package/netapps/authentik.netapp +0 -0
- package/netapps/caddy.netapp +0 -0
- package/netapps/greenwave.netapp +0 -0
- package/netapps/homebridge.netapp +0 -0
- package/netapps/iptables.netapp +0 -0
- package/netapps/knot-unbound-internal.netapp +0 -0
- package/netapps/namecheap.netapp +0 -0
- package/netapps/technitium.netapp +0 -0
- package/netapps/test-ansible-output.netapp +0 -0
- package/package.json +8 -1
- package/simulators/greenwave/state.ts +7 -12
- package/simulators/isitup/Caddyfile +11 -0
- package/simulators/isitup/entrypoint.sh +47 -0
- package/simulators/isitup/server.ts +112 -0
- package/simulators/namecheap-ddns/server.ts +31 -2
- package/src/cli/build.ts +214 -0
- package/src/cli/command-registry.ts +100 -0
- package/src/cli/command-tree-parser.ts +52 -0
- package/src/cli/completion.ts +195 -0
- package/src/cli/index.ts +273 -0
- package/src/cli/module-discovery.ts +120 -0
- package/src/cli/scaffold.ts +108 -0
- package/src/container-manager.ts +373 -76
- package/src/docker-compose-generator.ts +127 -23
- package/src/network-builder.ts +21 -0
- package/src/parse-line.ts +147 -0
- package/src/runner.ts +197 -247
- package/src/simulator-ips.ts +42 -0
- package/src/types.ts +28 -0
- package/bin/cache-docker-images +0 -46
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
FROM oven/bun:1
|
|
2
|
+
|
|
3
|
+
WORKDIR /app
|
|
4
|
+
|
|
5
|
+
# Install the shared @celilo/registry-server package.
|
|
6
|
+
# Both this e2e stand-in and the celilo-registry module run the same code —
|
|
7
|
+
# packages/registry-server is the single source of truth.
|
|
8
|
+
COPY registry-server/package.json ./package.json
|
|
9
|
+
COPY registry-server/tsconfig.json ./tsconfig.json
|
|
10
|
+
COPY registry-server/src ./src
|
|
11
|
+
|
|
12
|
+
RUN bun install --production
|
|
13
|
+
|
|
14
|
+
# Bootstrap mode: the shared-infra stand-in serves modules directly from the
|
|
15
|
+
# repo's modules/ directory (volume-mounted read-only). This preserves the
|
|
16
|
+
# pre-unification convenience where tests can `celilo module import
|
|
17
|
+
# public-registry namecheap` without having to publish namecheap first.
|
|
18
|
+
#
|
|
19
|
+
# The production celilo-registry module does NOT set BOOTSTRAP_MODULES_DIR
|
|
20
|
+
# and only serves pre-published packages.
|
|
21
|
+
ENV DATA_DIR=/var/lib/celilo-registry \
|
|
22
|
+
BOOTSTRAP_MODULES_DIR=/modules \
|
|
23
|
+
BOOTSTRAP_UPLOADS_DIR=/uploads \
|
|
24
|
+
BOOTSTRAP_CACHE_DIR=/cache \
|
|
25
|
+
PATH_PREFIX= \
|
|
26
|
+
PORT=80
|
|
27
|
+
|
|
28
|
+
# /uploads is the drop-zone for .netapps that tests push via
|
|
29
|
+
# `docker compose cp`. Create the dir at build time so `docker cp` has
|
|
30
|
+
# somewhere to put files before the server runs.
|
|
31
|
+
RUN mkdir -p /uploads
|
|
32
|
+
|
|
33
|
+
EXPOSE 80
|
|
34
|
+
CMD ["bun", "run", "src/index.ts"]
|
|
@@ -57,5 +57,25 @@ RUN (cd /lib/systemd/system/sysinit.target.wants/ && \
|
|
|
57
57
|
# Re-enable the services we actually need
|
|
58
58
|
RUN systemctl enable ssh target-setup
|
|
59
59
|
|
|
60
|
+
# --- Caddy pre-installation ---
|
|
61
|
+
# Temporarily disable apt proxy (not available during docker build)
|
|
62
|
+
RUN rm -f /etc/apt/apt.conf.d/00proxy
|
|
63
|
+
|
|
64
|
+
# Install Caddy (same steps as the Ansible role, but at image build time so
|
|
65
|
+
# the role finds it already installed and skips the slow cloudsmith.io downloads)
|
|
66
|
+
RUN apt-get update && apt-get install -y \
|
|
67
|
+
debian-keyring \
|
|
68
|
+
debian-archive-keyring \
|
|
69
|
+
apt-transport-https \
|
|
70
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
71
|
+
RUN curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | \
|
|
72
|
+
gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
|
|
73
|
+
RUN curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | \
|
|
74
|
+
tee /etc/apt/sources.list.d/caddy-stable.list
|
|
75
|
+
RUN apt-get update && apt-get install -y caddy && rm -rf /var/lib/apt/lists/*
|
|
76
|
+
|
|
77
|
+
# Re-enable apt proxy for runtime
|
|
78
|
+
RUN echo 'Acquire::http::Proxy "http://100.64.0.2:3142";' > /etc/apt/apt.conf.d/00proxy
|
|
79
|
+
|
|
60
80
|
STOPSIGNAL SIGRTMIN+3
|
|
61
81
|
ENTRYPOINT ["/sbin/init"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# Target machine with Docker pre-installed
|
|
2
|
-
# Used for app-zone machines that
|
|
3
|
-
#
|
|
1
|
+
# Target machine with Docker pre-installed.
|
|
2
|
+
# Used for app-zone machines that run Docker Compose workloads (e.g. authentik).
|
|
3
|
+
# Docker itself is baked in; application images are pulled at deploy time by each module.
|
|
4
4
|
|
|
5
5
|
FROM ubuntu:22.04
|
|
6
6
|
|
|
@@ -87,22 +87,14 @@ RUN mkdir -p /etc/docker && \
|
|
|
87
87
|
|
|
88
88
|
RUN systemctl enable docker
|
|
89
89
|
|
|
90
|
-
# Pre-cached
|
|
91
|
-
#
|
|
92
|
-
#
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
docker load < "$img"\n\
|
|
99
|
-
done\n\
|
|
100
|
-
fi\n' > /usr/local/bin/load-cached-images.sh && \
|
|
101
|
-
chmod +x /usr/local/bin/load-cached-images.sh
|
|
102
|
-
|
|
103
|
-
RUN printf '[Unit]\nDescription=Load pre-cached Docker images\nAfter=docker.service\nRequires=docker.service\n\n[Service]\nType=oneshot\nExecStart=/usr/local/bin/load-cached-images.sh\nRemainAfterExit=yes\n\n[Install]\nWantedBy=multi-user.target\n' \
|
|
104
|
-
> /etc/systemd/system/load-docker-cache.service && \
|
|
105
|
-
systemctl enable load-docker-cache
|
|
90
|
+
# Pre-load cached Docker images on startup (e2e acceleration)
|
|
91
|
+
# /docker-image-cache is bind-mounted from the host by the compose generator.
|
|
92
|
+
# The service is a no-op if the directory is absent or empty.
|
|
93
|
+
COPY config/routing/docker-image-preload.sh /usr/local/bin/docker-image-preload.sh
|
|
94
|
+
RUN chmod +x /usr/local/bin/docker-image-preload.sh
|
|
95
|
+
COPY config/routing/docker-image-preload.service /etc/systemd/system/docker-image-preload.service
|
|
96
|
+
RUN systemctl enable docker-image-preload
|
|
97
|
+
|
|
106
98
|
|
|
107
99
|
STOPSIGNAL SIGRTMIN+3
|
|
108
100
|
ENTRYPOINT ["/sbin/init"]
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Target machine with Technitium DNS Server pre-installed.
|
|
2
|
+
# Based on target-machine; Technitium is installed at image build time so the
|
|
3
|
+
# Ansible role finds it already present and skips the download step.
|
|
4
|
+
|
|
5
|
+
FROM ubuntu:22.04
|
|
6
|
+
|
|
7
|
+
ENV DEBIAN_FRONTEND=noninteractive
|
|
8
|
+
|
|
9
|
+
RUN apt-get update && apt-get install -y \
|
|
10
|
+
systemd \
|
|
11
|
+
systemd-sysv \
|
|
12
|
+
openssh-server \
|
|
13
|
+
iproute2 \
|
|
14
|
+
iputils-ping \
|
|
15
|
+
net-tools \
|
|
16
|
+
dnsutils \
|
|
17
|
+
procps \
|
|
18
|
+
python3 \
|
|
19
|
+
python3-apt \
|
|
20
|
+
sqlite3 \
|
|
21
|
+
jq \
|
|
22
|
+
sudo \
|
|
23
|
+
curl \
|
|
24
|
+
ca-certificates \
|
|
25
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
26
|
+
|
|
27
|
+
# Trust the E2E forward proxy CA (for transparent HTTPS interception)
|
|
28
|
+
COPY config/proxy/squid-ca.crt /usr/local/share/ca-certificates/e2e-proxy-ca.crt
|
|
29
|
+
# Trust Pebble's test CA (so Caddy/ACME clients trust the simulated Let's Encrypt)
|
|
30
|
+
COPY config/pebble/pebble-ca.crt /usr/local/share/ca-certificates/pebble-ca.crt
|
|
31
|
+
RUN update-ca-certificates
|
|
32
|
+
|
|
33
|
+
RUN mkdir -p /run/sshd && \
|
|
34
|
+
sed -i 's/#PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config && \
|
|
35
|
+
sed -i 's/#PubkeyAuthentication.*/PubkeyAuthentication yes/' /etc/ssh/sshd_config
|
|
36
|
+
|
|
37
|
+
# Enable SSH service
|
|
38
|
+
RUN systemctl enable ssh
|
|
39
|
+
|
|
40
|
+
# Point apt at the simulated apt cache proxy
|
|
41
|
+
RUN echo 'Acquire::http::Proxy "http://100.64.0.2:3142";' > /etc/apt/apt.conf.d/00proxy
|
|
42
|
+
|
|
43
|
+
# E2E network setup service (runs at boot to configure routing, DNS, SSH keys)
|
|
44
|
+
COPY config/routing/target-setup.sh /usr/local/bin/target-setup.sh
|
|
45
|
+
RUN chmod +x /usr/local/bin/target-setup.sh
|
|
46
|
+
|
|
47
|
+
COPY config/routing/target-setup.service /etc/systemd/system/target-setup.service
|
|
48
|
+
RUN systemctl enable target-setup
|
|
49
|
+
|
|
50
|
+
# Clean up unnecessary systemd services that slow down boot
|
|
51
|
+
RUN (cd /lib/systemd/system/sysinit.target.wants/ && \
|
|
52
|
+
ls | grep -v systemd-tmpfiles-setup | xargs rm -f 2>/dev/null); \
|
|
53
|
+
rm -f /lib/systemd/system/multi-user.target.wants/*; \
|
|
54
|
+
rm -f /etc/systemd/system/*.wants/*; \
|
|
55
|
+
rm -f /lib/systemd/system/local-fs.target.wants/*; \
|
|
56
|
+
rm -f /lib/systemd/system/sockets.target.wants/*udev*; \
|
|
57
|
+
rm -f /lib/systemd/system/sockets.target.wants/*initctl*; \
|
|
58
|
+
rm -f /lib/systemd/system/basic.target.wants/*; \
|
|
59
|
+
true
|
|
60
|
+
|
|
61
|
+
# Re-enable the services we actually need
|
|
62
|
+
RUN systemctl enable ssh target-setup
|
|
63
|
+
|
|
64
|
+
# --- Technitium pre-installation (manual — installer requires running systemd) ---
|
|
65
|
+
# The official install.sh checks for systemd at runtime and fails during docker build.
|
|
66
|
+
# Instead: install ASP.NET Core runtime, download binaries, write the service file.
|
|
67
|
+
# Ansible tasks check for /opt/technitium/dns/DnsServerApp.dll and skip the download.
|
|
68
|
+
|
|
69
|
+
# Temporarily lift the apt proxy (not available at build time)
|
|
70
|
+
RUN rm -f /etc/apt/apt.conf.d/00proxy
|
|
71
|
+
|
|
72
|
+
# ASP.NET Core 8.0 runtime via Microsoft package feed
|
|
73
|
+
RUN curl -sSL https://packages.microsoft.com/config/ubuntu/22.04/packages-microsoft-prod.deb \
|
|
74
|
+
-o /tmp/ms-prod.deb && \
|
|
75
|
+
dpkg -i /tmp/ms-prod.deb && \
|
|
76
|
+
rm /tmp/ms-prod.deb && \
|
|
77
|
+
apt-get update && \
|
|
78
|
+
apt-get install -y aspnetcore-runtime-8.0 && \
|
|
79
|
+
rm -rf /var/lib/apt/lists/*
|
|
80
|
+
|
|
81
|
+
# Download and extract Technitium portable package
|
|
82
|
+
RUN mkdir -p /opt/technitium/dns && \
|
|
83
|
+
curl -sSL https://download.technitium.com/dns/DnsServerPortable.tar.gz \
|
|
84
|
+
-o /tmp/technitium.tar.gz && \
|
|
85
|
+
tar -xzf /tmp/technitium.tar.gz -C /opt/technitium/dns && \
|
|
86
|
+
rm -f /tmp/technitium.tar.gz
|
|
87
|
+
|
|
88
|
+
# Register systemd service (same unit the installer creates)
|
|
89
|
+
RUN printf '[Unit]\nDescription=Technitium DNS Server\nAfter=network.target\n\n[Service]\nExecStart=/usr/bin/dotnet /opt/technitium/dns/DnsServerApp.dll\nRestart=on-failure\nRestartSec=10\n\n[Install]\nWantedBy=multi-user.target\n' \
|
|
90
|
+
> /etc/systemd/system/dns.service && \
|
|
91
|
+
systemctl enable dns
|
|
92
|
+
|
|
93
|
+
# Re-enable apt proxy for runtime
|
|
94
|
+
RUN echo 'Acquire::http::Proxy "http://100.64.0.2:3142";' > /etc/apt/apt.conf.d/00proxy
|
|
95
|
+
|
|
96
|
+
STOPSIGNAL SIGRTMIN+3
|
|
97
|
+
ENTRYPOINT ["/sbin/init"]
|
package/netapps/authentik.netapp
CHANGED
|
Binary file
|
package/netapps/caddy.netapp
CHANGED
|
Binary file
|
package/netapps/greenwave.netapp
CHANGED
|
Binary file
|
|
Binary file
|
package/netapps/iptables.netapp
CHANGED
|
Binary file
|
|
Binary file
|
package/netapps/namecheap.netapp
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@celilo/e2e",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "E2E test infrastructure for Celilo-deployed applications. Provides a simulated internet with DNS hierarchy, ACME server, firewalls, and target machines in Docker.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -10,21 +10,28 @@
|
|
|
10
10
|
"./fixtures": "./src/fixtures.ts"
|
|
11
11
|
},
|
|
12
12
|
"bin": {
|
|
13
|
+
"cele2e": "./bin/cele2e",
|
|
13
14
|
"celilo-e2e-run": "./bin/e2e-run",
|
|
14
15
|
"celilo-e2e-build": "./bin/e2e-build",
|
|
15
16
|
"celilo-e2e-load": "./bin/e2e-load",
|
|
16
17
|
"celilo-e2e-infra": "./bin/e2e-infra"
|
|
17
18
|
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"test": "bun test tests/",
|
|
21
|
+
"test:completion": "bun test tests/completion"
|
|
22
|
+
},
|
|
18
23
|
"files": [
|
|
19
24
|
"src/",
|
|
20
25
|
"docker/",
|
|
21
26
|
"config/",
|
|
27
|
+
"completions/",
|
|
22
28
|
"simulators/",
|
|
23
29
|
"bin/",
|
|
24
30
|
"netapps/",
|
|
25
31
|
"README.md"
|
|
26
32
|
],
|
|
27
33
|
"dependencies": {
|
|
34
|
+
"@celilo/cli-display": "0.1.0",
|
|
28
35
|
"yaml": "^2.8.0"
|
|
29
36
|
},
|
|
30
37
|
"devDependencies": {
|
|
@@ -161,28 +161,23 @@ function applyIptablesRule(mapping: PortMapping): void {
|
|
|
161
161
|
const intIp = mapping.internalClient;
|
|
162
162
|
const intPort = mapping.internalPort;
|
|
163
163
|
|
|
164
|
-
|
|
164
|
+
function addRules(p: string) {
|
|
165
165
|
// DNAT incoming traffic on external interface destined for our public IP
|
|
166
166
|
execSync(
|
|
167
|
-
`iptables -t nat -A PREROUTING -i ${EXTERNAL_INTERFACE} -d ${PUBLIC_IP} -p ${
|
|
167
|
+
`iptables -t nat -A PREROUTING -i ${EXTERNAL_INTERFACE} -d ${PUBLIC_IP} -p ${p} --dport ${extPort} -j DNAT --to-destination ${intIp}:${intPort}`,
|
|
168
168
|
{ timeout: 5000 },
|
|
169
169
|
);
|
|
170
170
|
// Allow forwarded traffic
|
|
171
171
|
execSync(
|
|
172
|
-
`iptables -A FORWARD -p ${
|
|
172
|
+
`iptables -A FORWARD -p ${p} -d ${intIp} --dport ${intPort} -j ACCEPT`,
|
|
173
173
|
{ timeout: 5000 },
|
|
174
174
|
);
|
|
175
|
+
}
|
|
175
176
|
|
|
176
|
-
|
|
177
|
+
try {
|
|
178
|
+
addRules(proto);
|
|
177
179
|
if (mapping.protocol === 'TCP/UDP') {
|
|
178
|
-
|
|
179
|
-
`iptables -t nat -A PREROUTING -i ${EXTERNAL_INTERFACE} -d ${PUBLIC_IP} -p udp --dport ${extPort} -j DNAT --to-destination ${intIp}:${intPort}`,
|
|
180
|
-
{ timeout: 5000 },
|
|
181
|
-
);
|
|
182
|
-
execSync(
|
|
183
|
-
`iptables -A FORWARD -p udp -d ${intIp} --dport ${intPort} -j ACCEPT`,
|
|
184
|
-
{ timeout: 5000 },
|
|
185
|
-
);
|
|
180
|
+
addRules('udp');
|
|
186
181
|
}
|
|
187
182
|
} catch (err) {
|
|
188
183
|
console.error(`[greenwave] Failed to apply iptables rule:`, err);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
email admin@isitup.org
|
|
3
|
+
# Pebble listens on 443 with a TLS cert whose SAN is
|
|
4
|
+
# acme-v02.api.letsencrypt.org (not an IP), so we use the hostname and
|
|
5
|
+
# rely on comcast-resolver's local-data override to reach 100.64.0.100.
|
|
6
|
+
acme_ca https://acme-v02.api.letsencrypt.org/dir
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
isitup.org {
|
|
10
|
+
reverse_proxy localhost:8080
|
|
11
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Start the Bun API server in the background, then Caddy in the foreground.
|
|
3
|
+
# Caddy terminates TLS (with a cert issued by Pebble) and reverse-proxies
|
|
4
|
+
# /api.json to Bun on localhost:8080.
|
|
5
|
+
|
|
6
|
+
set -e
|
|
7
|
+
|
|
8
|
+
# Route through fw-ext so we can reach comcast-resolver on isp-external
|
|
9
|
+
# (Docker's default bridge gateway has no path across networks). Without
|
|
10
|
+
# this, DNS for acme-v02.api.letsencrypt.org fails, Caddy never gets its
|
|
11
|
+
# cert, and port 443 never opens. Mirrors config/routing/pebble-startup.sh.
|
|
12
|
+
# fw-ext is a per-test container so this route is unreachable when shared
|
|
13
|
+
# infra runs alone — harmless; the test brings fw-ext up before the prober
|
|
14
|
+
# is exercised.
|
|
15
|
+
ip route del default 2>/dev/null || true
|
|
16
|
+
ip route add default via 100.64.0.1
|
|
17
|
+
|
|
18
|
+
# Fetch Pebble's runtime ACME root CA so the simulator trusts certs presented
|
|
19
|
+
# by the targets it probes (e.g. Caddy on iamtheinternet.org uses a Pebble-
|
|
20
|
+
# issued cert). Pebble regenerates its root on every startup, so the static
|
|
21
|
+
# pebble-ca.crt baked in at build time isn't enough.
|
|
22
|
+
echo "Waiting for Pebble ACME root CA..."
|
|
23
|
+
for i in $(seq 1 60); do
|
|
24
|
+
if curl -sk https://100.64.0.100:15000/roots/0 -o /usr/local/share/ca-certificates/pebble-acme-root.crt 2>/dev/null; then
|
|
25
|
+
if [ -s /usr/local/share/ca-certificates/pebble-acme-root.crt ]; then
|
|
26
|
+
update-ca-certificates 2>/dev/null
|
|
27
|
+
echo "Pebble ACME root CA installed"
|
|
28
|
+
break
|
|
29
|
+
fi
|
|
30
|
+
fi
|
|
31
|
+
sleep 2
|
|
32
|
+
done
|
|
33
|
+
|
|
34
|
+
cd /simulator
|
|
35
|
+
bun run server.ts &
|
|
36
|
+
BUN_PID=$!
|
|
37
|
+
|
|
38
|
+
# Wait briefly for Bun to come up before Caddy starts proxying
|
|
39
|
+
for i in $(seq 1 10); do
|
|
40
|
+
if curl -sf http://127.0.0.1:8080/health >/dev/null 2>&1; then
|
|
41
|
+
break
|
|
42
|
+
fi
|
|
43
|
+
sleep 0.3
|
|
44
|
+
done
|
|
45
|
+
|
|
46
|
+
# Caddy runs in foreground so the container lifecycle follows it
|
|
47
|
+
exec caddy run --config /etc/caddy/Caddyfile --adapter caddyfile
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* isitup.org Simulator
|
|
3
|
+
*
|
|
4
|
+
* Mimics the isitup.org JSON API:
|
|
5
|
+
* GET /api.json?url=<domain>
|
|
6
|
+
*
|
|
7
|
+
* status_code values match the real API:
|
|
8
|
+
* 1 = site is up
|
|
9
|
+
* 2 = site is down (connection failed / non-2xx/3xx)
|
|
10
|
+
* 3 = bad URL (malformed)
|
|
11
|
+
*
|
|
12
|
+
* The simulator is deployed on internet-external and probes the target
|
|
13
|
+
* from that network position — so it exercises the same public-facing
|
|
14
|
+
* path a real external prober would.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const PORT = Number(process.env.PORT) || 8080;
|
|
18
|
+
|
|
19
|
+
interface ProbeResponse {
|
|
20
|
+
domain: string;
|
|
21
|
+
port: number;
|
|
22
|
+
status_code: 1 | 2 | 3;
|
|
23
|
+
response_ip: string;
|
|
24
|
+
response_code: number;
|
|
25
|
+
response_time: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function probe(targetUrl: string): Promise<ProbeResponse> {
|
|
29
|
+
let parsed: URL;
|
|
30
|
+
const withScheme = targetUrl.includes('://') ? targetUrl : `https://${targetUrl}`;
|
|
31
|
+
try {
|
|
32
|
+
parsed = new URL(withScheme);
|
|
33
|
+
} catch {
|
|
34
|
+
return {
|
|
35
|
+
domain: targetUrl,
|
|
36
|
+
port: 443,
|
|
37
|
+
status_code: 3,
|
|
38
|
+
response_ip: '',
|
|
39
|
+
response_code: 0,
|
|
40
|
+
response_time: 0,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const port = parsed.port
|
|
45
|
+
? Number(parsed.port)
|
|
46
|
+
: parsed.protocol === 'https:'
|
|
47
|
+
? 443
|
|
48
|
+
: 80;
|
|
49
|
+
|
|
50
|
+
const start = Date.now();
|
|
51
|
+
try {
|
|
52
|
+
const response = await fetch(withScheme, {
|
|
53
|
+
signal: AbortSignal.timeout(10_000),
|
|
54
|
+
redirect: 'follow',
|
|
55
|
+
});
|
|
56
|
+
const responseTime = (Date.now() - start) / 1000;
|
|
57
|
+
// Real isitup treats 2xx/3xx as up. Anything else (or unreachable) is down.
|
|
58
|
+
const isUp = response.status >= 200 && response.status < 400;
|
|
59
|
+
console.log(`[isitup] ${withScheme} → HTTP ${response.status} in ${responseTime.toFixed(2)}s`);
|
|
60
|
+
return {
|
|
61
|
+
domain: parsed.hostname,
|
|
62
|
+
port,
|
|
63
|
+
status_code: isUp ? 1 : 2,
|
|
64
|
+
response_ip: '',
|
|
65
|
+
response_code: response.status,
|
|
66
|
+
response_time: responseTime,
|
|
67
|
+
};
|
|
68
|
+
} catch (error) {
|
|
69
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
70
|
+
console.log(`[isitup] ${withScheme} → probe error: ${message}`);
|
|
71
|
+
return {
|
|
72
|
+
domain: parsed.hostname,
|
|
73
|
+
port,
|
|
74
|
+
status_code: 2,
|
|
75
|
+
response_ip: '',
|
|
76
|
+
response_code: 0,
|
|
77
|
+
response_time: (Date.now() - start) / 1000,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const server = Bun.serve({
|
|
83
|
+
port: PORT,
|
|
84
|
+
async fetch(req) {
|
|
85
|
+
const url = new URL(req.url);
|
|
86
|
+
|
|
87
|
+
if (url.pathname === '/api.json') {
|
|
88
|
+
const target = url.searchParams.get('url');
|
|
89
|
+
if (!target) {
|
|
90
|
+
return Response.json({
|
|
91
|
+
domain: '',
|
|
92
|
+
port: 443,
|
|
93
|
+
status_code: 3,
|
|
94
|
+
response_ip: '',
|
|
95
|
+
response_code: 0,
|
|
96
|
+
response_time: 0,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
const result = await probe(target);
|
|
100
|
+
console.log(`[isitup] ${target} → ${result.status_code} (${result.response_code})`);
|
|
101
|
+
return Response.json(result);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (url.pathname === '/' || url.pathname === '/health') {
|
|
105
|
+
return Response.json({ ok: true, service: 'isitup.org simulator' });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return new Response('Not Found', { status: 404 });
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
console.log(`[isitup] simulator listening on port ${server.port}`);
|
|
@@ -11,9 +11,38 @@
|
|
|
11
11
|
import { updateZone } from './zone-updater';
|
|
12
12
|
import type { DdnsRecord } from './types';
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
// Real Namecheap DDNS uses a separate password per domain. The simulator
|
|
15
|
+
// supports two modes:
|
|
16
|
+
// 1. DDNS_PASSWORDS (JSON object mapping domain→password). Per-domain
|
|
17
|
+
// lookup; password must match the entry for the requested domain.
|
|
18
|
+
// 2. DDNS_PASSWORD (single string fallback). Accepted for any domain.
|
|
19
|
+
// Default `test123` keeps single-domain tests working unchanged.
|
|
20
|
+
// Mode 1 wins when set; the cross-domain e2e test exercises it via
|
|
21
|
+
// docker-compose env to verify per-domain password routing.
|
|
22
|
+
const PASSWORD_MAP: Record<string, string> = (() => {
|
|
23
|
+
const raw = process.env.DDNS_PASSWORDS;
|
|
24
|
+
if (!raw) return {};
|
|
25
|
+
try {
|
|
26
|
+
const parsed = JSON.parse(raw);
|
|
27
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
28
|
+
const out: Record<string, string> = {};
|
|
29
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
30
|
+
if (typeof v === 'string') out[k] = v;
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
} catch {
|
|
35
|
+
console.warn(`[ddns] Failed to parse DDNS_PASSWORDS env var: ${raw}`);
|
|
36
|
+
}
|
|
37
|
+
return {};
|
|
38
|
+
})();
|
|
39
|
+
const FALLBACK_PASSWORD = process.env.DDNS_PASSWORD || 'test123';
|
|
15
40
|
const PORT = Number(process.env.DDNS_PORT) || 8080;
|
|
16
41
|
|
|
42
|
+
function expectedPasswordFor(domain: string): string {
|
|
43
|
+
return PASSWORD_MAP[domain] ?? FALLBACK_PASSWORD;
|
|
44
|
+
}
|
|
45
|
+
|
|
17
46
|
// Track registered records for debugging
|
|
18
47
|
const records: Map<string, DdnsRecord> = new Map();
|
|
19
48
|
|
|
@@ -66,7 +95,7 @@ const server = Bun.serve({
|
|
|
66
95
|
return xmlResponse(1, '', ['Missing required parameters: host, domain']);
|
|
67
96
|
}
|
|
68
97
|
|
|
69
|
-
if (password !==
|
|
98
|
+
if (password !== expectedPasswordFor(domain)) {
|
|
70
99
|
return xmlResponse(1, ip, ['Passwords do not match']);
|
|
71
100
|
}
|
|
72
101
|
|