mirror of
https://github.com/Av0idD4rk/firegex.git
synced 2026-10-23 14:31:00 +00:00
Publish a browsable demo on GitHub Pages
The real frontend, built against an in-memory backend, so the UI can be tried without a Linux host, NET_ADMIN or a running firewall. A visitor can create, edit, start, stop and delete services across all five modules and watch the UI react as it would against a real instance; a reload restores the seed. - frontend/src/js/demo.ts holds the mock. genericapi() and the socket.io client are the frontend's only two ways out to the network, so routing both through it was enough - no component knows the difference. It also emits the same "update" events the backend does, and ticks the packet counters of running services so the page looks alive. - The demo lives behind IS_DEMO, which vite inlines as a literal, so a normal `bun run build` tree-shakes the module and its seed data away. Verified: no demo string survives in the production bundle. - The workflow rebuilds on every push touching frontend/ or docs/ and deploys to demo.firegex.pwnzer0tt1.it, copying index.html to 404.html so deep links resolve on Pages.
This commit is contained in:
parent
cb270b45ef
commit
08d83e0041
68
.github/workflows/demo-pages.yml
vendored
Normal file
68
.github/workflows/demo-pages.yml
vendored
Normal file
@ -0,0 +1,68 @@
|
||||
# Publishes the browsable demo at https://demo.firegex.pwnzer0tt1.it - the real frontend
|
||||
# built against the in-memory backend in frontend/src/js/demo.ts, so it needs no server.
|
||||
name: Deploy demo to GitHub Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'frontend/**'
|
||||
- 'docs/**'
|
||||
- '.github/workflows/demo-pages.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
# Let a running deploy finish rather than publishing a half-built site, but keep only the
|
||||
# newest queued commit: intermediate ones would be overwritten seconds later anyway.
|
||||
concurrency:
|
||||
group: pages
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: frontend
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- name: Build the demo
|
||||
working-directory: frontend
|
||||
run: bun run build:demo
|
||||
|
||||
- name: Prepare the artifact
|
||||
working-directory: frontend/dist
|
||||
run: |
|
||||
# GitHub Pages serves 404.html for any path it has no file for. Handing it the
|
||||
# SPA entrypoint is what makes deep links like /nfproxy/<id> resolve.
|
||||
cp index.html 404.html
|
||||
echo 'demo.firegex.pwnzer0tt1.it' > CNAME
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: frontend/dist
|
||||
|
||||
deploy:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
permissions:
|
||||
pages: write
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Configure Pages
|
||||
uses: actions/configure-pages@v5
|
||||
|
||||
- name: Deploy
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
1
frontend/.env.demo
Normal file
1
frontend/.env.demo
Normal file
@ -0,0 +1 @@
|
||||
VITE_DEMO=true
|
||||
@ -29,7 +29,8 @@
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview"
|
||||
"preview": "vite preview",
|
||||
"build:demo": "tsc && vite build --mode demo"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
|
||||
334
frontend/src/js/demo.ts
Normal file
334
frontend/src/js/demo.ts
Normal file
@ -0,0 +1,334 @@
|
||||
/**
|
||||
* In-memory stand-in for the firegex backend, used only by the public demo build
|
||||
* (`bun run build:demo`, i.e. VITE_DEMO=true).
|
||||
*
|
||||
* A normal build never ships this: `utils.tsx` guards every reference behind IS_DEMO,
|
||||
* which vite inlines as a literal `false`, so rollup drops the module and its seed data.
|
||||
* Keep it free of top-level side effects, or that tree-shaking stops working.
|
||||
*
|
||||
* It covers the subset of the API the frontend actually calls, over mutable state, so a
|
||||
* visitor can create, edit, start, stop and delete things and watch the UI react exactly
|
||||
* as it would against a real instance. Nothing is persisted: a reload restores the seed.
|
||||
*/
|
||||
|
||||
type Json = Record<string, any>
|
||||
|
||||
const uuid = () => (crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(16).slice(2))
|
||||
|
||||
const DEMO_CERT = "-----BEGIN CERTIFICATE-----\n(demo placeholder, no real key material is shipped)\n-----END CERTIFICATE-----\n"
|
||||
|
||||
const SAMPLE_FILTER = `from firegex.nfproxy import pyfilter, ACCEPT, REJECT
|
||||
from firegex.nfproxy.models import HttpRequest
|
||||
|
||||
|
||||
@pyfilter
|
||||
def block_path_traversal(packet: HttpRequest):
|
||||
"""Drop any request trying to climb out of the web root."""
|
||||
if ".." in packet.url:
|
||||
return REJECT
|
||||
return ACCEPT
|
||||
|
||||
|
||||
@pyfilter
|
||||
def strip_debug_header(packet: HttpRequest):
|
||||
if "X-Debug" in packet.headers:
|
||||
del packet.headers["X-Debug"]
|
||||
return ACCEPT
|
||||
`
|
||||
|
||||
// ------------------------------------------------------------------ seed state
|
||||
|
||||
const tlsStreamId = uuid()
|
||||
const svcWeb = uuid(), svcApi = uuid(), svcTls = uuid()
|
||||
const svcProxy = uuid(), svcQuiz = uuid()
|
||||
const svcHijack = uuid()
|
||||
|
||||
const state = {
|
||||
interfaces: [
|
||||
{ name: "lo", addr: "127.0.0.1" },
|
||||
{ name: "eth0", addr: "10.60.3.1" },
|
||||
{ name: "eth0", addr: "fd66:666:3::1" },
|
||||
],
|
||||
nfregex: {
|
||||
services: [
|
||||
{ name: "scoreboard", service_id: svcWeb, status: "active", port: 8080, proto: "tcp", ip_int: "10.60.3.1", n_packets: 128437, n_regex: 3, fail_open: true, target_type: "internal", tls_stream_id: null },
|
||||
{ name: "flag-submitter", service_id: svcApi, status: "active", port: 5000, proto: "tcp", ip_int: "10.60.3.1", n_packets: 41022, n_regex: 1, fail_open: false, target_type: "internal", tls_stream_id: null },
|
||||
{ name: "vault (behind TLS)", service_id: svcTls, status: "stop", port: 8443, proto: "tcp", ip_int: "10.60.3.1", n_packets: 0, n_regex: 1, fail_open: true, target_type: "tls", tls_stream_id: tlsStreamId },
|
||||
] as Json[],
|
||||
regexes: [
|
||||
{ id: 1, service_id: svcWeb, regex: "L2V0Yy9wYXNzd2Q=", is_case_sensitive: true, mode: "C", n_packets: 914, active: true },
|
||||
{ id: 2, service_id: svcWeb, regex: "PHNjcmlwdD4=", is_case_sensitive: false, mode: "C", n_packets: 233, active: true },
|
||||
{ id: 3, service_id: svcWeb, regex: "W0EtWjAtOV17MzF9PQ==", is_case_sensitive: true, mode: "S", n_packets: 57, active: false },
|
||||
{ id: 4, service_id: svcApi, regex: "dW5pb24Ic2VsZWN0", is_case_sensitive: false, mode: "C", n_packets: 1180, active: true },
|
||||
{ id: 5, service_id: svcTls, regex: "L3Byb2Mvc2VsZi8=", is_case_sensitive: true, mode: "C", n_packets: 0, active: true },
|
||||
] as Json[],
|
||||
},
|
||||
nfproxy: {
|
||||
services: [
|
||||
{ service_id: svcProxy, name: "shop-api", status: "active", port: 9000, proto: "tcp", ip_int: "10.60.3.1", n_filters: 2, edited_packets: 3401, blocked_packets: 762, fail_open: true, target_type: "internal", tls_stream_id: null },
|
||||
{ service_id: svcQuiz, name: "quiz", status: "stop", port: 1337, proto: "tcp", ip_int: "10.60.3.1", n_filters: 2, edited_packets: 0, blocked_packets: 0, fail_open: false, target_type: "internal", tls_stream_id: null },
|
||||
] as Json[],
|
||||
pyfilters: [
|
||||
{ name: "block_path_traversal", service_id: svcProxy, blocked_packets: 641, edited_packets: 0, active: true },
|
||||
{ name: "strip_debug_header", service_id: svcProxy, blocked_packets: 0, edited_packets: 3401, active: true },
|
||||
{ name: "block_path_traversal", service_id: svcQuiz, blocked_packets: 0, edited_packets: 0, active: true },
|
||||
{ name: "strip_debug_header", service_id: svcQuiz, blocked_packets: 0, edited_packets: 0, active: false },
|
||||
] as Json[],
|
||||
code: { [svcProxy]: SAMPLE_FILTER, [svcQuiz]: SAMPLE_FILTER } as Record<string, string>,
|
||||
tls: {} as Record<string, Json>,
|
||||
},
|
||||
porthijack: [
|
||||
{ name: "legacy ftp -> proxy", service_id: svcHijack, active: true, proto: "tcp", ip_src: "10.60.3.1", ip_dst: "127.0.0.1", public_port: 21, proxy_port: 12021 },
|
||||
] as Json[],
|
||||
firewall: {
|
||||
enabled: true,
|
||||
policy: "accept",
|
||||
rules: [
|
||||
{ active: true, name: "allow team vpn", proto: "any", src: "10.10.0.0/16", dst: "", port_src_from: 0, port_dst_from: 0, port_src_to: 0, port_dst_to: 0, action: "accept", mode: "in", table: "filter" },
|
||||
{ active: true, name: "drop ssh from outside", proto: "tcp", src: "0.0.0.0/0", dst: "", port_src_from: 0, port_dst_from: 22, port_src_to: 0, port_dst_to: 22, action: "drop", mode: "in", table: "filter" },
|
||||
{ active: false, name: "reject smtp egress", proto: "tcp", src: "", dst: "0.0.0.0/0", port_src_from: 0, port_dst_from: 25, port_src_to: 0, port_dst_to: 25, action: "reject", mode: "out", table: "filter" },
|
||||
] as Json[],
|
||||
settings: {
|
||||
keep_rules: false, allow_loopback: true, allow_established: true, allow_icmp: true,
|
||||
multicast_dns: false, allow_upnp: false, drop_invalid: true, allow_dhcp: true,
|
||||
} as Json,
|
||||
},
|
||||
tls: [
|
||||
{ id: tlsStreamId, name: "vault", ip_int: "10.60.3.1", port: 8443, cert: DEMO_CERT, key: "", status: "active", ssl_port: 41337, clear_port: 41338 },
|
||||
] as Json[],
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------- update events
|
||||
// Mirrors the backend's single "update" socket.io event: the payload is a react-query
|
||||
// key prefix, and App.tsx invalidates every query starting with it.
|
||||
|
||||
type Listener = (payload: string[]) => void
|
||||
const listeners: Record<string, Listener[]> = {}
|
||||
let ticker: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
const emit = (...tags: string[][]) => {
|
||||
for (const tag of tags) for (const cb of listeners["update"] ?? []) cb(tag)
|
||||
}
|
||||
|
||||
/** Nudges the counters of everything that is running, so the demo looks alive. */
|
||||
const startTicker = () => {
|
||||
if (ticker) return
|
||||
ticker = setInterval(() => {
|
||||
let changed = false
|
||||
for (const s of state.nfregex.services) {
|
||||
if (s.status !== "active") continue
|
||||
s.n_packets += Math.floor(Math.random() * 40)
|
||||
changed = true
|
||||
}
|
||||
for (const r of state.nfregex.regexes) {
|
||||
if (r.active && Math.random() < 0.3) r.n_packets += 1
|
||||
}
|
||||
for (const s of state.nfproxy.services) {
|
||||
if (s.status !== "active") continue
|
||||
s.edited_packets += Math.floor(Math.random() * 12)
|
||||
s.blocked_packets += Math.random() < 0.4 ? 1 : 0
|
||||
changed = true
|
||||
}
|
||||
if (changed) emit(["nfregex"], ["nfproxy"])
|
||||
}, 4000)
|
||||
}
|
||||
|
||||
let welcomed = false
|
||||
const welcome = () => {
|
||||
if (welcomed) return
|
||||
welcomed = true
|
||||
// Imported lazily so this module stays free of top-level side effects.
|
||||
import("@mantine/notifications").then(({ showNotification }) => showNotification({
|
||||
title: "This is a demo",
|
||||
message: "Everything here is fake data running in your browser - no firewall is attached. " +
|
||||
"Create, start and stop whatever you like; a reload puts it all back.",
|
||||
color: "cyan",
|
||||
autoClose: 8000,
|
||||
}))
|
||||
}
|
||||
|
||||
export const demoSocket = {
|
||||
auth: {} as Json,
|
||||
connected: false,
|
||||
connect() {
|
||||
this.connected = true
|
||||
startTicker()
|
||||
welcome()
|
||||
for (const cb of listeners["connect"] ?? []) cb([])
|
||||
},
|
||||
disconnect() { this.connected = false },
|
||||
on(event: string, cb: Listener) { (listeners[event] ??= []).push(cb) },
|
||||
off(event: string) { delete listeners[event] },
|
||||
emit() { /* the real client sends nothing the demo needs to act on */ },
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------- handlers
|
||||
|
||||
const ok = { status: "ok" }
|
||||
const notFound = (what: string) => { throw `${what} not found` }
|
||||
|
||||
const nfregexService = (id: string) => state.nfregex.services.find(s => s.service_id === id) ?? notFound("Service")
|
||||
const nfproxyService = (id: string) => state.nfproxy.services.find(s => s.service_id === id) ?? notFound("Service")
|
||||
const hijackService = (id: string) => state.porthijack.find(s => s.service_id === id) ?? notFound("Service")
|
||||
const tlsStream = (id: string) => state.tls.find(s => s.id === id) ?? notFound("Stream")
|
||||
|
||||
const countRegexes = (id: string) => state.nfregex.regexes.filter(r => r.service_id === id).length
|
||||
|
||||
type Handler = (m: RegExpMatchArray, body: Json) => any
|
||||
const routes: [string, RegExp, Handler][] = [
|
||||
|
||||
// ---- global
|
||||
["GET", /^status$/, () => ({ status: "run", loggined: true, version: "demo", auth_disabled: false })],
|
||||
["POST", /^login$/, () => ({ access_token: "demo-token", token_type: "bearer" })],
|
||||
["POST", /^set-password$/, () => ok],
|
||||
["POST", /^change-password$/, () => ({ ...ok, access_token: "demo-token" })],
|
||||
["GET", /^interfaces$/, () => state.interfaces],
|
||||
["POST", /^reset$/, () => { emit(["nfregex"], ["nfproxy"], ["porthijack"], ["firewall"], ["tls_streams"]); return ok }],
|
||||
["GET", /^export$/, () => ({ "firegex.db": { keys_values: [] }, note: "demo export - not a real backup" })],
|
||||
["POST", /^import$/, () => { emit(["nfregex"], ["nfproxy"], ["porthijack"], ["firewall"], ["tls_streams"]); return ok }],
|
||||
|
||||
// ---- nfregex
|
||||
["GET", /^nfregex\/services$/, () => state.nfregex.services],
|
||||
["POST", /^nfregex\/services$/, (_m, b) => {
|
||||
const service_id = uuid()
|
||||
state.nfregex.services.push({ status: "active", n_packets: 0, n_regex: 0, target_type: "internal", tls_stream_id: null, ...b, service_id })
|
||||
emit(["nfregex"])
|
||||
return { ...ok, service_id }
|
||||
}],
|
||||
["GET", /^nfregex\/services\/([^/]+)$/, m => nfregexService(m[1])],
|
||||
["DELETE", /^nfregex\/services\/([^/]+)$/, m => {
|
||||
state.nfregex.services = state.nfregex.services.filter(s => s.service_id !== m[1])
|
||||
state.nfregex.regexes = state.nfregex.regexes.filter(r => r.service_id !== m[1])
|
||||
emit(["nfregex"]); return ok
|
||||
}],
|
||||
["POST", /^nfregex\/services\/([^/]+)\/(start|stop)$/, m => {
|
||||
nfregexService(m[1]).status = m[2] === "start" ? "active" : "stop"
|
||||
emit(["nfregex"]); return ok
|
||||
}],
|
||||
["PUT", /^nfregex\/services\/([^/]+)\/rename$/, (m, b) => { nfregexService(m[1]).name = b.name; emit(["nfregex"]); return ok }],
|
||||
["PUT", /^nfregex\/services\/([^/]+)\/settings$/, (m, b) => { Object.assign(nfregexService(m[1]), b); emit(["nfregex"]); return ok }],
|
||||
["PUT", /^nfregex\/services\/([^/]+)\/tls-config$/, () => ok],
|
||||
["GET", /^nfregex\/services\/([^/]+)\/regexes$/, m => state.nfregex.regexes.filter(r => r.service_id === m[1])],
|
||||
["GET", /^nfregex\/services\/([^/]+)\/export$/, m => state.nfregex.regexes.filter(r => r.service_id === m[1])],
|
||||
["POST", /^nfregex\/services\/([^/]+)\/import$/, m => { emit(["nfregex"]); return ok }],
|
||||
["POST", /^nfregex\/regexes$/, (_m, b) => {
|
||||
const id = Math.max(0, ...state.nfregex.regexes.map(r => r.id)) + 1
|
||||
state.nfregex.regexes.push({ n_packets: 0, active: true, ...b, id })
|
||||
nfregexService(b.service_id).n_regex = countRegexes(b.service_id)
|
||||
emit(["nfregex"]); return ok
|
||||
}],
|
||||
["DELETE", /^nfregex\/regexes\/(\d+)$/, m => {
|
||||
const r = state.nfregex.regexes.find(x => x.id === Number(m[1]))
|
||||
state.nfregex.regexes = state.nfregex.regexes.filter(x => x.id !== Number(m[1]))
|
||||
if (r) nfregexService(r.service_id).n_regex = countRegexes(r.service_id)
|
||||
emit(["nfregex"]); return ok
|
||||
}],
|
||||
["POST", /^nfregex\/regexes\/(\d+)\/(enable|disable)$/, m => {
|
||||
const r = state.nfregex.regexes.find(x => x.id === Number(m[1])) ?? notFound("Regex")
|
||||
r.active = m[2] === "enable"
|
||||
emit(["nfregex"]); return ok
|
||||
}],
|
||||
|
||||
// ---- nfproxy
|
||||
["GET", /^nfproxy\/services$/, () => state.nfproxy.services],
|
||||
["POST", /^nfproxy\/services$/, (_m, b) => {
|
||||
const service_id = uuid()
|
||||
state.nfproxy.services.push({ status: "stop", n_filters: 0, edited_packets: 0, blocked_packets: 0, target_type: "internal", tls_stream_id: null, ...b, service_id })
|
||||
state.nfproxy.code[service_id] = ""
|
||||
emit(["nfproxy"]); return { ...ok, service_id }
|
||||
}],
|
||||
["GET", /^nfproxy\/services\/([^/]+)$/, m => nfproxyService(m[1])],
|
||||
["DELETE", /^nfproxy\/services\/([^/]+)$/, m => {
|
||||
state.nfproxy.services = state.nfproxy.services.filter(s => s.service_id !== m[1])
|
||||
state.nfproxy.pyfilters = state.nfproxy.pyfilters.filter(f => f.service_id !== m[1])
|
||||
emit(["nfproxy"]); return ok
|
||||
}],
|
||||
["POST", /^nfproxy\/services\/([^/]+)\/(start|stop)$/, m => {
|
||||
nfproxyService(m[1]).status = m[2] === "start" ? "active" : "stop"
|
||||
emit(["nfproxy"]); return ok
|
||||
}],
|
||||
["PUT", /^nfproxy\/services\/([^/]+)\/rename$/, (m, b) => { nfproxyService(m[1]).name = b.name; emit(["nfproxy"]); return ok }],
|
||||
["PUT", /^nfproxy\/services\/([^/]+)\/settings$/, (m, b) => { Object.assign(nfproxyService(m[1]), b); emit(["nfproxy"]); return ok }],
|
||||
["PUT", /^nfproxy\/services\/([^/]+)\/tls-config$/, () => ok],
|
||||
["GET", /^nfproxy\/services\/([^/]+)\/tls-config$/, () => ({ tls_enabled: false, tls_cert: null, tls_key: null })],
|
||||
["GET", /^nfproxy\/services\/([^/]+)\/pyfilters$/, m => state.nfproxy.pyfilters.filter(f => f.service_id === m[1])],
|
||||
["GET", /^nfproxy\/services\/([^/]+)\/code$/, m => ({ code: state.nfproxy.code[m[1]] ?? "" })],
|
||||
["POST", /^nfproxy\/services\/([^/]+)\/code$/, (m, b) => {
|
||||
state.nfproxy.code[m[1]] = b?.code ?? ""
|
||||
emit(["nfproxy"]); return ok
|
||||
}],
|
||||
["POST", /^nfproxy\/services\/([^/]+)\/pyfilters\/([^/]+)\/(enable|disable)$/, m => {
|
||||
const f = state.nfproxy.pyfilters.find(x => x.service_id === m[1] && x.name === m[2]) ?? notFound("Filter")
|
||||
f.active = m[3] === "enable"
|
||||
emit(["nfproxy"]); return ok
|
||||
}],
|
||||
|
||||
// ---- porthijack
|
||||
["GET", /^porthijack\/services$/, () => state.porthijack],
|
||||
["POST", /^porthijack\/services$/, (_m, b) => {
|
||||
const service_id = uuid()
|
||||
state.porthijack.push({ active: false, ...b, service_id })
|
||||
emit(["porthijack"]); return { ...ok, service_id }
|
||||
}],
|
||||
["GET", /^porthijack\/services\/([^/]+)$/, m => hijackService(m[1])],
|
||||
["DELETE", /^porthijack\/services\/([^/]+)$/, m => {
|
||||
state.porthijack = state.porthijack.filter(s => s.service_id !== m[1])
|
||||
emit(["porthijack"]); return ok
|
||||
}],
|
||||
["POST", /^porthijack\/services\/([^/]+)\/(start|stop)$/, m => {
|
||||
hijackService(m[1]).active = m[2] === "start"
|
||||
emit(["porthijack"]); return ok
|
||||
}],
|
||||
["PUT", /^porthijack\/services\/([^/]+)\/rename$/, (m, b) => { hijackService(m[1]).name = b.name; emit(["porthijack"]); return ok }],
|
||||
["PUT", /^porthijack\/services\/([^/]+)\/change-destination$/, (m, b) => { Object.assign(hijackService(m[1]), b); emit(["porthijack"]); return ok }],
|
||||
|
||||
// ---- firewall
|
||||
["GET", /^firewall\/rules$/, () => ({ rules: state.firewall.rules, policy: state.firewall.policy, enabled: state.firewall.enabled })],
|
||||
["POST", /^firewall\/rules$/, (_m, b) => {
|
||||
state.firewall.rules = b?.rules ?? []
|
||||
if (b?.policy) state.firewall.policy = b.policy
|
||||
emit(["firewall"]); return { status: state.firewall.rules.map((_r, i) => ({ status: "ok", rule_id: i })) }
|
||||
}],
|
||||
["GET", /^firewall\/settings$/, () => state.firewall.settings],
|
||||
["PUT", /^firewall\/settings$/, (_m, b) => { Object.assign(state.firewall.settings, b); emit(["firewall"]); return ok }],
|
||||
["POST", /^firewall\/(enable|disable)$/, m => { state.firewall.enabled = m[1] === "enable"; emit(["firewall"]); return ok }],
|
||||
|
||||
// ---- tls decrypt
|
||||
["GET", /^tls\/streams$/, () => state.tls],
|
||||
["POST", /^tls\/streams$/, (_m, b) => {
|
||||
const id = uuid()
|
||||
// The real backend derives both loopback ports from a hash of ip:port.
|
||||
const h = Math.abs([...`${b.ip_int}:${b.port}`].reduce((a, c) => a * 31 + c.charCodeAt(0) | 0, 7))
|
||||
state.tls.push({ status: "active", ssl_port: 40000 + (h % 10000), clear_port: 40001 + (h % 10000), ...b, id })
|
||||
emit(["tls_streams"]); return { ...ok, id }
|
||||
}],
|
||||
["PUT", /^tls\/streams\/([^/]+)$/, (m, b) => { Object.assign(tlsStream(m[1]), b); emit(["tls_streams"]); return ok }],
|
||||
["DELETE", /^tls\/streams\/([^/]+)$/, m => {
|
||||
if (state.nfregex.services.some(s => s.tls_stream_id === m[1]) || state.nfproxy.services.some(s => s.tls_stream_id === m[1]))
|
||||
throw "Stream is still used by a service"
|
||||
state.tls = state.tls.filter(s => s.id !== m[1])
|
||||
emit(["tls_streams"]); return ok
|
||||
}],
|
||||
["POST", /^tls\/streams\/([^/]+)\/(start|stop)$/, m => {
|
||||
const stream = tlsStream(m[1])
|
||||
stream.status = m[2] === "start" ? "active" : "stop"
|
||||
// Stopping a stream cascades to the filter services attached to it.
|
||||
if (m[2] === "stop") {
|
||||
for (const s of state.nfregex.services) if (s.tls_stream_id === m[1]) s.status = "stop"
|
||||
for (const s of state.nfproxy.services) if (s.tls_stream_id === m[1]) s.status = "stop"
|
||||
}
|
||||
emit(["tls_streams"], ["nfregex"], ["nfproxy"]); return ok
|
||||
}],
|
||||
]
|
||||
|
||||
/** Same contract as genericapi(): resolves with the parsed body, rejects with a message. */
|
||||
export async function demoApi(method: string, path: string, body: Json | undefined): Promise<any> {
|
||||
const clean = path.replace(/^\/+|\/+$/g, "").split("?")[0]
|
||||
await new Promise(r => setTimeout(r, 60 + Math.random() * 120)) // a plausible round trip
|
||||
for (const [verb, pattern, handler] of routes) {
|
||||
if (verb !== method.toUpperCase()) continue
|
||||
const match = clean.match(pattern)
|
||||
if (match) return handler(match, body ?? {})
|
||||
}
|
||||
throw `This endpoint is not available in the demo (${method} /api/${clean})`
|
||||
}
|
||||
@ -6,10 +6,15 @@ import { ChangePassword, IpInterface, LoginResponse, PasswordSend, ServerRespons
|
||||
import { Buffer } from "buffer"
|
||||
import { QueryClient, useQuery } from "@tanstack/react-query";
|
||||
import { useMediaQuery } from "@mantine/hooks";
|
||||
import { io } from "socket.io-client";
|
||||
import { io, type Socket } from "socket.io-client";
|
||||
import { useAuthStore, useSessionStore } from "./store";
|
||||
import { demoApi, demoSocket } from "./demo";
|
||||
|
||||
export const IS_DEV = import.meta.env.DEV
|
||||
// The public demo build (`bun run build:demo`) answers every request from an in-memory
|
||||
// backend instead of the network. Vite inlines this as a literal, so a normal build drops
|
||||
// ./demo and its seed data entirely.
|
||||
export const IS_DEMO = import.meta.env.VITE_DEMO === "true"
|
||||
|
||||
export const regex_ipv6 = "^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*(\\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8]))?$";
|
||||
export const regex_ipv6_no_cidr = "^s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]d|1dd|[1-9]?d)(.(25[0-5]|2[0-4]d|1dd|[1-9]?d)){3}))|:)))(%.+)?s*$";
|
||||
@ -28,7 +33,10 @@ export function typeCastEnum<E>(value: EnumToPrimitiveUnion<E>): E {
|
||||
return value as E;
|
||||
}
|
||||
|
||||
export const socketio = import.meta.env.DEV?
|
||||
// demoSocket implements the handful of members App.tsx uses; the cast keeps every
|
||||
// call site typed against the real client.
|
||||
export const socketio: Socket = IS_DEMO? (demoSocket as unknown as Socket) :
|
||||
import.meta.env.DEV?
|
||||
io("ws://"+DEV_IP_BACKEND, {
|
||||
path:"/sock/socket.io",
|
||||
transports: ['websocket'],
|
||||
@ -82,6 +90,7 @@ export function getErrorMessageFromServerResponse(e: any, def:string = "Unknown
|
||||
|
||||
|
||||
export async function genericapi(method:string,path:string,data:any = undefined, is_form:boolean=false):Promise<any>{
|
||||
if (IS_DEMO) return await demoApi(method, path, data)
|
||||
return await new Promise((resolve, reject) => {
|
||||
fetch(`${IS_DEV?`http://${DEV_IP_BACKEND}`:""}/api/${path}`, {
|
||||
method: method,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user