ipflow/templates/index.html

329 lines
9.9 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>DWS IPFlow</title>
<!-- Google Fonts -->
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,300italic,700,700italic">
<!-- CSS Reset -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/purecss@3.0.0/build/pure-min.css"
integrity="sha384-X38yfunGUhNzHpBaEBsWLO+A0HDYOQi8ufWDkZ0k9e0eXz/tH3II7uKZ9msv++Ls" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/purecss@3.0.0/build/grids-responsive-min.css">
<style>
.pure-g>div {
box-sizing: border-box;
}
</style>
<!-- You should properly set the path from the main file. -->
</head>
<body style="height:100vh; width: 100%;">
<div class="container" style="height: 100%;">
<div class="pure-g" style="height: 100%;">
<div class="pure-u-1-4"
style="height: 100%; padding: .5em; z-index: 10; box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 12px;">
<form id="parse-form" method="post" class="pure-form pure-form-stacke" style="height: 100%;">
<div style="display: flex; flex-direction: column; height: 100%; gap: .25em;">
<div>
<h3>IP Tables Visualizer</h3>
</div>
<div style="flex-grow: 2;">
<textarea placeholder="Paste the output of 'sudo iptables -L -v' here" id="parsedata" name="parsedata"
class=" pure-input"
style=" font-family:Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New, monospace; white-space: pre; overflow-x: scroll; width: 100%; resize: none; height: 100%;"></textarea>
</div>
<div style="width 100%">
<input class="pure-button pure-button-primary" type="submit" value="Render" style="width: 100%;">
</div>
</div>
</form>
</div>
<div class="pure-u-3-4" style="height: 100%;">
<div id="svg-container" style="height: 100%;">
<svg width="1" height="1"></svg>
</div>
</div>
</div>
</div>
<script src="https://code.jquery.com/jquery-3.7.1.min.js"
integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script>
<script src="https://d3js.org/d3.v7.min.js"></script>
<script type="text/javascript">
color = d3.scaleOrdinal(d3.schemeDark2);
background_color = 'white';
constructTangleLayout = (levels, options = {}) => {
// precompute level depth
levels.forEach((l, i) => l.forEach(n => (n.level = i)));
var nodes = levels.reduce((a, x) => a.concat(x), []);
var nodes_index = {};
nodes.forEach(d => (nodes_index[d.id] = d));
// objectification
nodes.forEach(d => {
d.parents = (d.parents === undefined ? [] : d.parents).map(
p => nodes_index[p]
);
});
// precompute bundles
levels.forEach((l, i) => {
var index = {};
l.forEach(n => {
if (n.parents.length == 0) {
return;
}
var id = n.parents
.map(d => d.id)
.sort()
.join('-X-');
if (id in index) {
index[id].parents = index[id].parents.concat(n.parents);
} else {
index[id] = {id: id, parents: n.parents.slice(), level: i, span: i - d3.min(n.parents, p => p.level)};
}
n.bundle = index[id];
});
l.bundles = Object.keys(index).map(k => index[k]);
l.bundles.forEach((b, i) => (b.i = i));
});
var links = [];
nodes.forEach(d => {
d.parents.forEach(p =>
links.push({source: d, bundle: d.bundle, target: p})
);
});
var bundles = levels.reduce((a, x) => a.concat(x.bundles), []);
// reverse pointer from parent to bundles
bundles.forEach(b =>
b.parents.forEach(p => {
if (p.bundles_index === undefined) {
p.bundles_index = {};
}
if (!(b.id in p.bundles_index)) {
p.bundles_index[b.id] = [];
}
p.bundles_index[b.id].push(b);
})
);
nodes.forEach(n => {
if (n.bundles_index !== undefined) {
n.bundles = Object.keys(n.bundles_index).map(k => n.bundles_index[k]);
} else {
n.bundles_index = {};
n.bundles = [];
}
n.bundles.sort((a, b) => d3.descending(d3.max(a, d => d.span), d3.max(b, d => d.span)))
n.bundles.forEach((b, i) => (b.i = i));
});
links.forEach(l => {
if (l.bundle.links === undefined) {
l.bundle.links = [];
}
l.bundle.links.push(l);
});
// layout
const padding = 8;
const node_height = 48;
const node_width = 150;
const bundle_width = 14;
const level_y_padding = 16;
const metro_d = 4;
const min_family_height = 22;
options.c ||= 16;
const c = options.c;
options.bigc ||= node_width + c;
nodes.forEach(
n => (n.height = (Math.max(1, n.bundles.length) - 1) * metro_d)
);
var x_offset = padding;
var y_offset = padding;
levels.forEach(l => {
x_offset += l.bundles.length * bundle_width;
y_offset += level_y_padding;
l.forEach((n, i) => {
n.x = n.level * node_width + x_offset;
n.y = node_height + y_offset + n.height / 2;
y_offset += node_height + n.height;
});
});
var i = 0;
levels.forEach(l => {
l.bundles.forEach(b => {
b.x =
d3.max(b.parents, d => d.x) +
node_width +
(l.bundles.length - 1 - b.i) * bundle_width;
b.y = i * node_height;
});
i += l.length;
});
links.forEach(l => {
l.xt = l.target.x;
l.yt =
l.target.y +
l.target.bundles_index[l.bundle.id].i * metro_d -
(l.target.bundles.length * metro_d) / 2 +
metro_d / 2;
l.xb = l.bundle.x;
l.yb = l.bundle.y;
l.xs = l.source.x;
l.ys = l.source.y;
});
// compress vertical space
var y_negative_offset = 0;
levels.forEach(l => {
y_negative_offset +=
-min_family_height +
d3.min(l.bundles, b =>
d3.min(b.links, link => link.ys - 2 * c - (link.yt + c))
) || 0;
l.forEach(n => (n.y -= y_negative_offset));
});
// very ugly, I know
links.forEach(l => {
l.yt =
l.target.y +
l.target.bundles_index[l.bundle.id].i * metro_d -
(l.target.bundles.length * metro_d) / 2 +
metro_d / 2;
l.ys = l.source.y;
l.c1 = l.source.level - l.target.level > 1 ? Math.min(options.bigc, l.xb - l.xt, l.yb - l.yt) - c : c;
l.c2 = c;
});
var layout = {
width: d3.max(nodes, n => n.x) + node_width + 2 * padding,
height: d3.max(nodes, n => n.y) + node_height / 2 + 2 * padding,
node_height,
node_width,
bundle_width,
level_y_padding,
metro_d
};
return {levels, nodes, nodes_index, links, bundles, layout};
};
renderChart = (tangleLayout, options = {}) => {
options.color ||= (d, i) => color(i)
return `<svg width="${tangleLayout.layout.width}" height="${tangleLayout.layout.height
}" style="background-color: ${background_color}">
<style>
text {
font-family: sans-serif;
font-size: 10px;
}
.node {
stroke-linecap: round;
}
.link {
fill: none;
}
</style>
<g>
${tangleLayout.bundles.map((b, i) => {
let d = b.links
.map(
l => `
M${l.xt} ${l.yt}
L${l.xb - l.c1} ${l.yt}
A${l.c1} ${l.c1} 90 0 1 ${l.xb} ${l.yt + l.c1}
L${l.xb} ${l.ys - l.c2}
A${l.c2} ${l.c2} 90 0 0 ${l.xb + l.c2} ${l.ys}
L${l.xs} ${l.ys}`
)
.join("");
return `
<path class="link" d="${d}" stroke="${background_color}" stroke-width="5"/>
<path class="link" d="${d}" stroke="${options.color(b, i)}" stroke-width="2"/>
`;
})}
${tangleLayout.nodes.map(
n => `
<path class="selectable node" data-id="${n.id
}" stroke="black" stroke-width="8" d="M${n.x} ${n.y - n.height / 2} L${n.x
} ${n.y + n.height / 2}"/>
<path class="node" stroke="white" stroke-width="4" d="M${n.x} ${n.y -
n.height / 2} L${n.x} ${n.y + n.height / 2}"/>
<text class="selectable" data-id="${n.id}" x="${n.x + 4}" y="${n.y -
n.height / 2 -
4}" stroke="${background_color}" stroke-width="2">${n.id}</text>
<text x="${n.x + 4}" y="${n.y -
n.height / 2 -
4}" style="pointer-events: none;">${n.id}</text>
`
)}
</g>
</svg>`;
}
$(document).on('submit', '#parse-form', function (e) {
e.preventDefault();
$.ajax({
type: 'POST',
url: '/parse',
data: {
data: $("#parsedata").val()
},
success: function (result) {
console.log(result);
const tangleLayout = constructTangleLayout(result);
chart = renderChart(tangleLayout)
$("#svg-container").html(chart);
svg = d3.select('svg');
var zoom = d3.zoom()
.on("zoom", function (event) {
svg.select("g").attr("transform", event.transform);
});
svg.call(zoom);
svg.attr("width", "100%");
svg.attr("height", "100%");
}
})
});
window.addEventListener("load", (event) => {
});
</script>
</body>
</html>