This commit is contained in:
Ilya Starchak
2025-12-10 02:27:35 +03:00
parent c237112077
commit d8061985d6
4 changed files with 521 additions and 1 deletions

223
backend/routers/setup.py Normal file
View File

@@ -0,0 +1,223 @@
from fastapi import APIRouter, HTTPException, UploadFile, File
from pydantic import BaseModel
import json
from typing import List, Optional
from utils.models import StatusMessageModel
from routers import nfproxy, nfregex, porthijack, firewall
class ServiceConfig(BaseModel):
name: str
port: int
proto: str
ip_int: str
fail_open: bool = True
class PortHijackServiceConfig(BaseModel):
name: str
public_port: int
proxy_port: int
proto: str
ip_src: str
ip_dst: str
class FirewallRuleConfig(BaseModel):
mode: str
src: str
dst: str
in_int: str
out_int: str
proto: str
sport: str
dport: str
class SetupConfig(BaseModel):
services: Optional[List[ServiceConfig]] = []
porthijack: Optional[List[PortHijackServiceConfig]] = []
firewall: Optional[List[FirewallRuleConfig]] = []
class SetupResponse(BaseModel):
status: str
services_created: int = 0
porthijack_created: int = 0
firewall_created: int = 0
errors: List[str] = []
app = APIRouter()
@app.post("/import", response_model=SetupResponse)
async def import_setup(config: SetupConfig):
"""
Import services and rules from a setup configuration.
Creates basic services without filters or regex rules.
"""
errors = []
services_count = 0
porthijack_count = 0
firewall_count = 0
# Import Services
if config.services:
for service_config in config.services:
try:
# Determine which module to use based on protocol
# HTTP -> NFProxy, TCP/UDP -> can use either (prefer NFProxy)
if service_config.proto in ["tcp", "http", "udp"]:
# Create NFProxy service
try:
add_form = nfproxy.ServiceAddForm(
name=service_config.name,
port=service_config.port,
proto=service_config.proto,
ip_int=service_config.ip_int,
fail_open=service_config.fail_open
)
result = await nfproxy.add_service(add_form)
if result.status == "ok":
services_count += 1
else:
errors.append(f"Service '{service_config.name}': Failed to create")
except Exception as e:
errors.append(f"Service '{service_config.name}': {str(e)}")
else:
errors.append(f"Service '{service_config.name}': Unsupported protocol '{service_config.proto}'")
except Exception as e:
errors.append(f"Service '{service_config.name}': {str(e)}")
# Import PortHijack services
if config.porthijack:
for service_config in config.porthijack:
try:
add_form = porthijack.ServiceAddForm(
name=service_config.name,
public_port=service_config.public_port,
proxy_port=service_config.proxy_port,
proto=service_config.proto,
ip_src=service_config.ip_src,
ip_dst=service_config.ip_dst
)
result = await porthijack.add_service(add_form)
if result.status == "ok":
porthijack_count += 1
else:
errors.append(f"PortHijack service '{service_config.name}': Failed to create")
except Exception as e:
errors.append(f"PortHijack service '{service_config.name}': {str(e)}")
# Import Firewall rules
if config.firewall:
for rule_config in config.firewall:
try:
rule_form = firewall.RuleFormAdd(
mode=rule_config.mode,
src=rule_config.src,
dst=rule_config.dst,
in_int=rule_config.in_int,
out_int=rule_config.out_int,
proto=rule_config.proto,
sport=rule_config.sport,
dport=rule_config.dport
)
await firewall.add_rule(rule_form)
firewall_count += 1
except Exception as e:
errors.append(f"Firewall rule: {str(e)}")
return SetupResponse(
status="ok" if len(errors) == 0 else "partial",
services_created=services_count,
porthijack_created=porthijack_count,
firewall_created=firewall_count,
errors=errors
)
@app.post("/import/file")
async def import_setup_file(file: UploadFile = File(...)):
"""
Import services from an uploaded JSON file.
"""
try:
content = await file.read()
config_dict = json.loads(content.decode('utf-8'))
config = SetupConfig(**config_dict)
return await import_setup(config)
except json.JSONDecodeError as e:
raise HTTPException(status_code=400, detail=f"Invalid JSON: {str(e)}")
except Exception as e:
raise HTTPException(status_code=400, detail=f"Error processing file: {str(e)}")
@app.get("/export")
async def export_setup():
"""
Export all current services and rules as a JSON configuration.
Exports only service definitions without filters or regexes.
"""
config = {
"services": [],
"porthijack": [],
"firewall": []
}
# Export NFProxy services
try:
nfproxy_services = await nfproxy.get_services()
for service in nfproxy_services:
config["services"].append({
"name": service.name,
"port": service.port,
"proto": service.proto,
"ip_int": service.ip_int,
"fail_open": service.fail_open
})
except:
pass
# Export NFRegex services
try:
nfregex_services = await nfregex.get_services()
for service in nfregex_services:
config["services"].append({
"name": service.name,
"port": service.port,
"proto": service.proto,
"ip_int": service.ip_int,
"fail_open": service.fail_open
})
except:
pass
# Export PortHijack services
try:
porthijack_services = await porthijack.get_services()
for service in porthijack_services:
config["porthijack"].append({
"name": service.name,
"public_port": service.public_port,
"proxy_port": service.proxy_port,
"proto": service.proto,
"ip_src": service.ip_src,
"ip_dst": service.ip_dst
})
except:
pass
# Export Firewall rules
try:
fw_rules = await firewall.get_rules()
for rule in fw_rules:
config["firewall"].append({
"mode": rule.mode,
"src": rule.src,
"dst": rule.dst,
"in_int": rule.in_int,
"out_int": rule.out_int,
"proto": rule.proto,
"sport": rule.sport,
"dport": rule.dport
})
except:
pass
return config

View File

@@ -0,0 +1,278 @@
import { Box, Button, Code, Divider, FileButton, Group, List, Paper, Space, Stack, Text, Textarea, ThemeIcon, Title } from '@mantine/core';
import { useState } from 'react';
import { FaCheck, FaDownload, FaExclamationTriangle, FaTimes, FaUpload } from 'react-icons/fa';
import { MdSettings } from 'react-icons/md';
import { getapi, isMediumScreen, postapi } from '../../js/utils';
import { errorNotify, successNotify } from '../../js/utils';
export default function SetupPage() {
const [file, setFile] = useState<File | null>(null);
const [importing, setImporting] = useState(false);
const [exporting, setExporting] = useState(false);
const [importResult, setImportResult] = useState<any>(null);
const [configJson, setConfigJson] = useState('');
const isMedium = isMediumScreen();
const handleExport = async () => {
setExporting(true);
try {
const response = await getapi('/setup/export');
const blob = new Blob([JSON.stringify(response, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `firegex-setup-${new Date().toISOString().split('T')[0]}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
successNotify('Configuration exported successfully');
} catch (err) {
errorNotify('Failed to export configuration', String(err));
} finally {
setExporting(false);
}
};
const handleImportFile = async () => {
if (!file) {
errorNotify('Please select a file first', '');
return;
}
setImporting(true);
setImportResult(null);
try {
const formData = new FormData();
formData.append('file', file);
const response = await fetch('/api/setup/import/file', {
method: 'POST',
body: formData
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Import failed');
}
const result = await response.json();
setImportResult(result);
if (result.status === 'ok') {
successNotify('Configuration imported successfully');
} else {
errorNotify('Configuration imported with errors', 'Check the results below');
}
} catch (err) {
errorNotify('Failed to import configuration', String(err));
} finally {
setImporting(false);
}
};
const handleImportJson = async () => {
if (!configJson.trim()) {
errorNotify('Please enter a JSON configuration', '');
return;
}
setImporting(true);
setImportResult(null);
try {
const config = JSON.parse(configJson);
const result = await postapi('/setup/import', config);
setImportResult(result);
if (result.status === 'ok') {
successNotify('Configuration imported successfully');
} else {
errorNotify('Configuration imported with errors', 'Check the results below');
}
} catch (err) {
if (err instanceof SyntaxError) {
errorNotify('Invalid JSON format', String(err));
} else {
errorNotify('Failed to import configuration', String(err));
}
} finally {
setImporting(false);
}
};
return (
<Box px="md" mt="lg">
<Title order={1} className="center-flex">
<ThemeIcon radius="md" size="lg" variant='filled' color='cyan'>
<MdSettings size={24} />
</ThemeIcon>
<Space w="sm" />
Setup Import/Export
</Title>
<Text c="dimmed" mt="sm">
Import or export your Firegex configuration including all services and rules
</Text>
<Divider my="lg" />
<Stack gap="xl">
{/* Export Section */}
<Paper shadow="sm" p="lg" withBorder>
<Title order={3} mb="md">Export Configuration</Title>
<Text c="dimmed" mb="md">
Download all current services and rules as a JSON file
</Text>
<Button
leftSection={<FaDownload />}
onClick={handleExport}
loading={exporting}
color="teal"
size="md"
>
Export to JSON
</Button>
</Paper>
{/* Import from File Section */}
<Paper shadow="sm" p="lg" withBorder>
<Title order={3} mb="md">Import from File</Title>
<Text c="dimmed" mb="md">
Upload a setup.json file to create services and rules
</Text>
<Group>
<FileButton onChange={setFile} accept="application/json">
{(props) => (
<Button {...props} variant="outline" color="cyan">
{file ? file.name : 'Select JSON File'}
</Button>
)}
</FileButton>
<Button
leftSection={<FaUpload />}
onClick={handleImportFile}
loading={importing}
disabled={!file}
color="blue"
size="md"
>
Import from File
</Button>
</Group>
</Paper>
{/* Import from JSON Section */}
<Paper shadow="sm" p="lg" withBorder>
<Title order={3} mb="md">Import from JSON</Title>
<Text c="dimmed" mb="md">
Paste a JSON configuration directly
</Text>
<Textarea
placeholder='{"services": [], "porthijack": [], "firewall": []}'
value={configJson}
onChange={(e) => setConfigJson(e.currentTarget.value)}
minRows={10}
maxRows={20}
mb="md"
styles={{ input: { fontFamily: 'monospace', fontSize: '12px' } }}
/>
<Button
leftSection={<FaUpload />}
onClick={handleImportJson}
loading={importing}
disabled={!configJson.trim()}
color="blue"
size="md"
>
Import from JSON
</Button>
</Paper>
{/* Import Results */}
{importResult && (
<Paper shadow="sm" p="lg" withBorder>
<Title order={3} mb="md">
<Group>
{importResult.status === 'ok' ? (
<ThemeIcon color="teal" size="lg" radius="xl">
<FaCheck />
</ThemeIcon>
) : (
<ThemeIcon color="yellow" size="lg" radius="xl">
<FaExclamationTriangle />
</ThemeIcon>
)}
Import Results
</Group>
</Title>
<Stack gap="md">
<Group>
<Text fw={500}>Services:</Text>
<Text c={importResult.services_created > 0 ? "teal" : "dimmed"}>
{importResult.services_created} created
</Text>
</Group>
<Group>
<Text fw={500}>PortHijack Services:</Text>
<Text c={importResult.porthijack_created > 0 ? "teal" : "dimmed"}>
{importResult.porthijack_created} created
</Text>
</Group>
<Group>
<Text fw={500}>Firewall Rules:</Text>
<Text c={importResult.firewall_created > 0 ? "teal" : "dimmed"}>
{importResult.firewall_created} created
</Text>
</Group>
{importResult.errors && importResult.errors.length > 0 && (
<>
<Divider />
<Text fw={500} c="red">Errors:</Text>
<List
spacing="xs"
size="sm"
icon={
<ThemeIcon color="red" size={20} radius="xl">
<FaTimes size={12} />
</ThemeIcon>
}
>
{importResult.errors.map((error: string, idx: number) => (
<List.Item key={idx}>
<Code>{error}</Code>
</List.Item>
))}
</List>
</>
)}
</Stack>
</Paper>
)}
{/* Example Configuration */}
<Paper shadow="sm" p="lg" withBorder>
<Title order={3} mb="md">Example Configuration</Title>
<Text c="dimmed" mb="md">
Here's an example of the JSON structure:
</Text>
<Code block>{`{
"services": [
{
"name": "Example HTTP Service",
"port": 8080,
"proto": "http",
"ip_int": "0.0.0.0",
"fail_open": true
}
],
"porthijack": [],
"firewall": []
}`}</Code>
</Paper>
</Stack>
</Box>
);
}

View File

@@ -254,7 +254,6 @@ export default function TrafficViewer() {
<MdDoubleArrow size="24px" />
</ActionIcon>
</Tooltip>
</Tooltip>
</Group>
</Box>
</Group>

20
setup.example.json Normal file
View File

@@ -0,0 +1,20 @@
{
"services": [
{
"name": "Example HTTP Service",
"port": 8080,
"proto": "http",
"ip_int": "0.0.0.0",
"fail_open": true
},
{
"name": "Example TCP Service",
"port": 443,
"proto": "tcp",
"ip_int": "0.0.0.0",
"fail_open": false
}
],
"porthijack": [],
"firewall": []
}