added Malcolm
This commit is contained in:
523
Vagrant/resources/malcolm/name-map-ui/site/index.html
Normal file
523
Vagrant/resources/malcolm/name-map-ui/site/index.html
Normal file
@@ -0,0 +1,523 @@
|
||||
<!doctype html>
|
||||
|
||||
<!-- Copyright (c) 2021 Battelle Energy Alliance, LLC. All rights reserved. -->
|
||||
|
||||
<!--[if lt IE 7 ]> <html lang="en" class="no-js ie6"> <![endif]-->
|
||||
<!--[if IE 7 ]> <html lang="en" class="no-js ie7"> <![endif]-->
|
||||
<!--[if IE 8 ]> <html lang="en" class="no-js ie8"> <![endif]-->
|
||||
<!--[if IE 9 ]> <html lang="en" class="no-js ie9"> <![endif]-->
|
||||
<!--[if (gt IE 9)|!(IE)]><!--> <html lang="en" class="no-js"> <!--<![endif]-->
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
<link rel="stylesheet" href="name-map-ui/mapping.css">
|
||||
<script src="name-map-ui/jquery.min.js"></script>
|
||||
<title>Host and Network Segment Name Mapping</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div id="container" class="mapping-page">
|
||||
<div id="main">
|
||||
<div class="c1">
|
||||
<img class="center" src="name-map-ui/Malcolm_banner.png" alt="">
|
||||
<h1>Host and Network Segment Name Mapping</h1>
|
||||
<div id="mapping">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="sort" title="Mapping type (host or network segment)" data-sort="type">Type</th>
|
||||
<th class="sort" title="Host address (IP or MAC) or CIDR-formatted IP range" data-sort="address">Address</th>
|
||||
<th class="sort" title="Host or network segment name" data-sort="name">Name</th>
|
||||
<th class="sort" title="Assign name only if the event contains this tag (optional)" data-sort="tag">Tag</th>
|
||||
<th colspan="2">
|
||||
<input type="text" class="search" placeholder="🔎 Search mappings" />
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="list"/>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td class="type">
|
||||
<input type="hidden" id="id-field" />
|
||||
<select id="type-field">
|
||||
<option value="host">🖳 host</option>
|
||||
<option value="segment">🖧 segment</option>
|
||||
</select>
|
||||
</td>
|
||||
<td class="address">
|
||||
<input type="text" id="address-field" placeholder="Address" />
|
||||
</td>
|
||||
<td class="name">
|
||||
<input type="text" id="name-field" placeholder="Name" />
|
||||
</td>
|
||||
<td class="tag">
|
||||
<input type="text" id="tag-field" placeholder="Tag (optional)" />
|
||||
</td>
|
||||
<td class="add" colspan="2">
|
||||
<button title="Create new mapping" class="add-btn" id="add-btn">💾</button>
|
||||
<button title="Modify mapping" class="update-btn" id="update-btn">💾</button>
|
||||
<button title="Cancel changes" class="cancel-btn" id="cancel-btn">❌</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="foot">
|
||||
<input type="file" id="import-file" name="import-file" style="display:none;"/>
|
||||
<button title="Import mappings from a JSON file" class="import-btn" id="import-btn">📥 Import</button>
|
||||
</td>
|
||||
<td class="foot">
|
||||
<button title="Download mappings as a JSON file" class="export-btn" id="export-btn">📤 Export</button>
|
||||
</td>
|
||||
<td class="foot" colspan="2">
|
||||
<button title="Write mappings to Malcolm's net-map.json" class="save-btn" id="save-btn">💾 Save Mappings</button>
|
||||
<input type="hidden" value=0 id="save-state"/>
|
||||
</td>
|
||||
<td class="foot" colspan="2">
|
||||
<button title="Restart log ingestion, parsing and enrichment" class="restart-btn" id="restart-btn">🔁 Restart Logstash</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> <!-- end of #container .mapping-page -->
|
||||
|
||||
<script src="name-map-ui/list.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
|
||||
// define value names and template for new list items
|
||||
var options = {
|
||||
valueNames: [ 'id', 'type', 'address', 'name', 'tag' ],
|
||||
item: '<tr><td class="id" style="display:none;"><td class="type"></td></td><td class="address"></td><td class="name"></td><td class="tag"></td><td class="update"><button title="Modify this mapping" class="edit-item-btn">📝</button></td><td class="remove"><button title="Remove this entry" class="remove-item-btn">🚫</button></td></tr>'
|
||||
};
|
||||
|
||||
// initialize list and other elements
|
||||
var mappingList = new List('mapping', options);
|
||||
|
||||
var idField = $('#id-field'),
|
||||
typeField = $('#type-field'),
|
||||
addressField = $('#address-field'),
|
||||
nameField = $('#name-field'),
|
||||
tagField = $('#tag-field'),
|
||||
addBtn = $('#add-btn'),
|
||||
updateBtn = $('#update-btn').hide(),
|
||||
cancelBtn = $('#cancel-btn').hide(),
|
||||
saveBtn = $('#save-btn'),
|
||||
saveState = $('#save-state'),
|
||||
exportBtn = $('#export-btn'),
|
||||
importBtn = $('#import-btn'),
|
||||
importFile = $('#import-file'),
|
||||
restartBtn = $('#restart-btn'),
|
||||
removeBtns = $('.remove-item-btn'),
|
||||
editBtns = $('.edit-item-btn');
|
||||
|
||||
// sets callbacks to the buttons in the list
|
||||
refreshCallbacks();
|
||||
|
||||
function focusItem(itemId) {
|
||||
// scroll back up to item that was updated
|
||||
// todo: is there a more efficient way to do this? there's got to be with list.js
|
||||
for (const editBtnKey in editBtns) {
|
||||
if (editBtnKey && (editBtns[editBtnKey])) {
|
||||
entry = (editBtns[editBtnKey].closest) ? editBtns[editBtnKey].closest('tr').firstChild : null;
|
||||
if (entry && entry.firstChild && entry.firstChild.data && (String(entry.firstChild.data) === String(itemId))) {
|
||||
editBtns[editBtnKey].focus();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} // focusItem
|
||||
|
||||
// store a new value from the edit inputs into the list
|
||||
addBtn.click(function() {
|
||||
const type = typeField.val();
|
||||
const address = addressField.val().trim();
|
||||
const name = nameField.val().trim();
|
||||
const tag = tagField.val().trim();
|
||||
if ((file_types_valid.has(type)) &&
|
||||
(address) && (address.length > 0) &&
|
||||
(name) && (name.length > 0)) {
|
||||
mappingList.add({
|
||||
// todo: better random ID generator (if necessary)
|
||||
id: Math.floor(Math.random()*110000),
|
||||
type: type,
|
||||
address: address,
|
||||
name: name,
|
||||
tag: tag
|
||||
});
|
||||
saveState.val(parseInt(saveState.val()) + 1);
|
||||
clearFields();
|
||||
refreshCallbacks();
|
||||
}
|
||||
});
|
||||
|
||||
// update an item being edited back into the list
|
||||
updateBtn.click(function() {
|
||||
const itemId = idField.val();
|
||||
const type = typeField.val();
|
||||
const address = addressField.val().trim();
|
||||
const name = nameField.val().trim();
|
||||
const tag = tagField.val().trim();
|
||||
if ((file_types_valid.has(type)) &&
|
||||
(address) && (address.length > 0) &&
|
||||
(name) && (name.length > 0)) {
|
||||
var item = mappingList.get('id', itemId)[0];
|
||||
item.values({
|
||||
id: itemId,
|
||||
type: type,
|
||||
address: addressField.val(),
|
||||
name: nameField.val(),
|
||||
tag: tagField.val()
|
||||
});
|
||||
saveState.val(parseInt(saveState.val()) + 1);
|
||||
clearFields();
|
||||
updateBtn.hide();
|
||||
cancelBtn.hide();
|
||||
addBtn.show();
|
||||
focusItem(itemId);
|
||||
}
|
||||
});
|
||||
|
||||
// revert edits without updating
|
||||
cancelBtn.click(function() {
|
||||
const itemId = idField.val();
|
||||
clearFields();
|
||||
updateBtn.hide();
|
||||
cancelBtn.hide();
|
||||
addBtn.show();
|
||||
focusItem(itemId);
|
||||
});
|
||||
|
||||
function hasJsonStructure(str) {
|
||||
if (typeof str !== 'string') return false;
|
||||
try {
|
||||
const result = JSON.parse(str);
|
||||
const type = Object.prototype.toString.call(result);
|
||||
return type === '[object Object]'
|
||||
|| type === '[object Array]';
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function download(filename, text, contentType="text/plain") {
|
||||
var element = document.createElement('a');
|
||||
element.setAttribute('href', 'data:' + contentType + ';charset=utf-8,' + encodeURIComponent(text));
|
||||
element.setAttribute('download', filename);
|
||||
element.style.display = 'none';
|
||||
document.body.appendChild(element);
|
||||
element.click();
|
||||
document.body.removeChild(element);
|
||||
}
|
||||
|
||||
async function uploadNameMap(jsonStr) {
|
||||
let formData = new FormData();
|
||||
let upBlob = new Blob([jsonStr], { type: 'application/json' });
|
||||
formData.append("upfile", upBlob);
|
||||
try {
|
||||
let r = await fetch('name-map-ui/upload.php', {method: "POST", body: formData});
|
||||
} catch(e) {
|
||||
console.log('uploadNameMap error: ', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function restartLogstash() {
|
||||
let formData = new FormData();
|
||||
formData.append("save-state", saveState.val());
|
||||
try {
|
||||
let r = await fetch('name-map-ui/restart-logstash.php', {method: "POST", body: formData});
|
||||
} catch(e) {
|
||||
console.log('restartLogstash error: ', e);
|
||||
}
|
||||
}
|
||||
|
||||
saveBtn.click(function() {
|
||||
if ((mappingList) && (confirm("Save name mappings?") === true )) {
|
||||
// create list of all items (minus the random "id" index field)
|
||||
let items = mappingList.items.map(
|
||||
function(item) { return (({ id, ...o }) => o)(item.values()); }
|
||||
);
|
||||
uploadNameMap(JSON.stringify(items, null, 2));
|
||||
saveState.val(0);
|
||||
}
|
||||
});
|
||||
|
||||
exportBtn.click(function() {
|
||||
if (mappingList) {
|
||||
// create list of all items (minus the random "id" index field)
|
||||
let items = mappingList.items.map(
|
||||
function(item) { return (({ id, ...o }) => o)(item.values()); }
|
||||
);
|
||||
download('net-map.json', JSON.stringify(items, null, 2), "application/json");
|
||||
}
|
||||
});
|
||||
|
||||
importFile.change(function (event) {
|
||||
if ((event) && (event.target) && (event.target.files) && (event.target.files.length > 0)) {
|
||||
let f = event.target.files[0];
|
||||
if (((f.type === 'application/json') || (f.type === 'text/plain')) && (f.size <= 67108864)) {
|
||||
let reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
if (hasJsonStructure(e.target.result)) {
|
||||
// replace current map list with that from imported file
|
||||
populateMapList([{fileType: null,
|
||||
fileSrc: file_spec_in_memory,
|
||||
filePath: e.target.result}], -1);
|
||||
|
||||
} else {
|
||||
alert('Invalid file format: ' + f.name + ', ' + f.type + ', ' + f.size + ' bytes');
|
||||
}
|
||||
};
|
||||
reader.readAsText(f);
|
||||
} else {
|
||||
alert('Invalid file: ' + f.name + ', ' + f.type + ', ' + f.size + ' bytes');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
importBtn.click(function() {
|
||||
if ((mappingList) && (confirm("Clear and replace current name mappings with those from a file?") === true )) {
|
||||
importFile.click();
|
||||
}
|
||||
});
|
||||
|
||||
restartBtn.click(function() {
|
||||
let confirmMsg = '';
|
||||
if (parseInt(saveState.val()) > 0) {
|
||||
saveBtn.click();
|
||||
}
|
||||
confirmMsg = confirmMsg.concat("Apply the saved name mappings and restart Logstash?");
|
||||
if (confirm(confirmMsg) == true ) {
|
||||
restartLogstash();
|
||||
alert("Logstash is restarting in the background.\nLog ingestion will be resumed in a few minutes.");
|
||||
}
|
||||
});
|
||||
|
||||
// apply callbacks for the buttons on the list items
|
||||
function refreshCallbacks() {
|
||||
|
||||
removeBtns = $(removeBtns.selector);
|
||||
editBtns = $(editBtns.selector);
|
||||
|
||||
// delete item
|
||||
removeBtns.click(function() {
|
||||
var itemId = $(this).closest('tr').find('.id').text();
|
||||
mappingList.remove('id', itemId);
|
||||
saveState.val(parseInt(saveState.val()) + 1);
|
||||
});
|
||||
|
||||
// populate the edit inputs from the values of row where Edit was clicked
|
||||
editBtns.click(function() {
|
||||
var itemId = $(this).closest('tr').find('.id').text();
|
||||
var itemValues = mappingList.get('id', itemId)[0].values();
|
||||
idField.val(itemValues.id);
|
||||
typeField.val(itemValues.type);
|
||||
addressField.val(itemValues.address);
|
||||
nameField.val(itemValues.name);
|
||||
tagField.val(itemValues.tag);
|
||||
|
||||
updateBtn.show();
|
||||
cancelBtn.show();
|
||||
addBtn.hide();
|
||||
window.scrollTo(0,document.body.scrollHeight);
|
||||
// focus and scroll to editing fields
|
||||
addressField.focus();
|
||||
addressField.select();
|
||||
});
|
||||
}
|
||||
|
||||
// clear edit inputs
|
||||
function clearFields() {
|
||||
typeField.val('host');
|
||||
addressField.val('');
|
||||
nameField.val('');
|
||||
tagField.val('');
|
||||
}
|
||||
|
||||
const file_type_txt_cidr = 'segment';
|
||||
const file_type_txt_host = 'host';
|
||||
const file_types_valid = new Set([file_type_txt_cidr, file_type_txt_host]);
|
||||
|
||||
const file_spec_on_server = 'SERVER';
|
||||
const file_spec_in_memory = 'LOCALSTR';
|
||||
const file_specs_valid = new Set([file_spec_on_server, file_spec_in_memory]);
|
||||
|
||||
|
||||
// given the text of a mapping file (eg., net-map.json, cidr-map.txt, host-map.txt)
|
||||
// populate an array containing the mapping entries from that file
|
||||
function parseMapFileText(fileTxt, mapType=null) {
|
||||
let result;
|
||||
|
||||
if (hasJsonStructure(fileTxt)) {
|
||||
// is already JSON text, should be the format we want
|
||||
result = JSON.parse(fileTxt);
|
||||
|
||||
} else {
|
||||
// parse the lines from the cidr-map.txt/host-map.txt format
|
||||
result = [];
|
||||
const lines = fileTxt.split(/\r?\n/);
|
||||
for (lineIdx in lines) {
|
||||
let line = lines[lineIdx];
|
||||
if (!line.startsWith("#")) {
|
||||
const vals = line.split("|");
|
||||
const valsLen = vals.length;
|
||||
if ((valsLen >= 2) && (valsLen < 4)) {
|
||||
const name = vals[1].trim();
|
||||
const tag = (valsLen > 2) ? vals[2].trim() : "";
|
||||
const addrs = vals[0].trim().split(",");
|
||||
for (addrIdx in addrs) {
|
||||
result.push({
|
||||
type: mapType,
|
||||
address: addrs[addrIdx],
|
||||
name: name,
|
||||
tag: tag
|
||||
});
|
||||
}
|
||||
} // line has the right number of delimited fields
|
||||
} // line is not a # comment
|
||||
} // for (lineIdx in lines)
|
||||
} // if/else hasJsonStructure
|
||||
|
||||
if (!Array.isArray(result)) {
|
||||
result = null;
|
||||
}
|
||||
return result;
|
||||
|
||||
} // parseMapFileText
|
||||
|
||||
|
||||
// given an array of filespecs (eg.,
|
||||
// [{fileType: file_type_txt_cidr, filePath: 'maps/cidr-map.txt'},
|
||||
// {fileType: file_type_txt_host, filePath: 'maps/host-map.txt'},
|
||||
// {fileType: null, filePath: 'maps/net-map.json'}]
|
||||
// read and return an array of the mappings within those files
|
||||
|
||||
function loadMapsFromFiles(fileSpecs, cb) {
|
||||
|
||||
let result = []
|
||||
|
||||
if ((fileSpecs.length > 0) && (fileSpecs[0]) && (file_specs_valid.has(fileSpecs[0].fileSrc))) {
|
||||
|
||||
// if this is a delimited file (not JSON) mark the type
|
||||
let mapType = null;
|
||||
if (fileSpecs[0].fileType === file_type_txt_cidr) {
|
||||
mapType = "segment";
|
||||
} else if (fileSpecs[0].fileType === file_type_txt_host) {
|
||||
mapType = "host";
|
||||
}
|
||||
// else the type is per-item in the JSON
|
||||
|
||||
if (fileSpecs[0].fileSrc === file_spec_in_memory) {
|
||||
// the text of the file is already in memory
|
||||
result = parseMapFileText(fileSpecs[0].filePath, mapType);
|
||||
return cb(result);
|
||||
|
||||
} else {
|
||||
// GET the file from the server
|
||||
var txtFile = new XMLHttpRequest();
|
||||
txtFile.open("GET", fileSpecs[0].filePath, true);
|
||||
txtFile.send();
|
||||
txtFile.onreadystatechange = function() {
|
||||
if (txtFile.status === 200) {
|
||||
if (txtFile.readyState === 4) {
|
||||
result = parseMapFileText(txtFile.responseText, mapType);
|
||||
if (fileSpecs.length > 1) {
|
||||
// we have processed this fileSpec, process the next
|
||||
loadMapsFromFiles(fileSpecs.slice(1), function(nextFileResult) {
|
||||
return cb(result.concat(nextFileResult));
|
||||
});
|
||||
} else {
|
||||
// we have processed this fileSpec, and there are no more to process
|
||||
return cb(result);
|
||||
}
|
||||
} // txtFile.readyState is ready
|
||||
|
||||
} else if (fileSpecs.length > 1) {
|
||||
// the GET returned an error, process the next fileSpec
|
||||
loadMapsFromFiles(fileSpecs.slice(1), function(nextFileResult) {
|
||||
return cb(result.concat(nextFileResult));
|
||||
});
|
||||
|
||||
} else {
|
||||
// the GET returned an error, and there are no more fileSpecs to process
|
||||
return cb(result);
|
||||
}
|
||||
|
||||
} // txtFile.onreadystatechange
|
||||
}
|
||||
|
||||
} else if (fileSpecs.length > 1) {
|
||||
// the first fileSpec is invalid, process the next
|
||||
loadMapsFromFiles(fileSpecs.slice(1), function(nextFileResult) {
|
||||
return cb(result.concat(nextFileResult));
|
||||
});
|
||||
|
||||
} else {
|
||||
// the first fileSpec is missing or invalid, and there are no more to process
|
||||
return cb(result);
|
||||
}
|
||||
|
||||
} // loadMapsFromFiles
|
||||
|
||||
function populateMapList(fileSpecs, newSaveState=0) {
|
||||
// load old delimited plain text format
|
||||
// IP or MAC address to host name map:
|
||||
// address|host name|required tag
|
||||
// CIDR to network segment format:
|
||||
// IP(s)|segment name|required tag
|
||||
// and JSON-formatted native format:
|
||||
// [
|
||||
// {
|
||||
// "type": "segment",
|
||||
// "address": "172.16.0.0/24",
|
||||
// "name": "home",
|
||||
// "tag": ""
|
||||
// }, ...
|
||||
mappingList.clear();
|
||||
loadMapsFromFiles(fileSpecs, function (mapsArray) {
|
||||
|
||||
// convert to a hash to resolve any duplicates
|
||||
var mapsHash = mapsArray.reduce(function(acc, cur) {
|
||||
acc[cur.type + '|' + cur.address] = cur;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// populate the list.js List object
|
||||
for (mapKey in mapsHash) {
|
||||
let map = mapsHash[mapKey];
|
||||
if ((file_types_valid.has(map.type)) &&
|
||||
(map.address) && (map.address.length > 0) &&
|
||||
(map.name) && (map.name.length > 0)) {
|
||||
mappingList.add({
|
||||
// todo: better random ID generator (if necessary)
|
||||
id: Math.floor(Math.random()*110000),
|
||||
type: map.type,
|
||||
address: map.address,
|
||||
name: map.name,
|
||||
tag: map.tag ? map.tag : ""
|
||||
});
|
||||
}
|
||||
}
|
||||
mappingList.sort('address');
|
||||
refreshCallbacks();
|
||||
saveState.val((newSaveState >= 0) ? newSaveState : Math.max(Object.keys(mapsHash).length, 1));
|
||||
});
|
||||
} // populateMapList
|
||||
|
||||
// initial page load from cidr-map.txt, host-map.txt, and/or net-map.json
|
||||
populateMapList([{fileType: file_type_txt_cidr,
|
||||
fileSrc: file_spec_on_server,
|
||||
filePath: 'name-map-ui/maps/cidr-map.txt',},
|
||||
{fileType: file_type_txt_host,
|
||||
fileSrc: file_spec_on_server,
|
||||
filePath: 'name-map-ui/maps/host-map.txt'},
|
||||
{fileType: null,
|
||||
fileSrc: file_spec_on_server,
|
||||
filePath: 'name-map-ui/maps/net-map.json'}]);
|
||||
|
||||
</script> <!-- end of ListJS-related scripts -->
|
||||
|
||||
</body>
|
||||
</html>
|
||||
229
Vagrant/resources/malcolm/name-map-ui/site/mapping.css
Normal file
229
Vagrant/resources/malcolm/name-map-ui/site/mapping.css
Normal file
@@ -0,0 +1,229 @@
|
||||
/* Copyright (c) 2021 Battelle Energy Alliance, LLC. All rights reserved. */
|
||||
|
||||
html {
|
||||
font-family: sans-serif;
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
color: #cecece;
|
||||
text-align: left;
|
||||
background-color: #272B30;
|
||||
}
|
||||
|
||||
body,
|
||||
div,
|
||||
dl,
|
||||
dt,
|
||||
dd,
|
||||
ul,
|
||||
ol,
|
||||
li,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
pre,
|
||||
form,
|
||||
fieldset,
|
||||
input,
|
||||
textarea,
|
||||
p,
|
||||
blockquote,
|
||||
th,
|
||||
td {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
fieldset,
|
||||
img {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
img.center {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul,
|
||||
dl {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem
|
||||
}
|
||||
|
||||
caption,
|
||||
th {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
header {
|
||||
float: left;
|
||||
margin-bottom: 20px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
address {
|
||||
margin-bottom: 1rem;
|
||||
line-height: inherit
|
||||
}
|
||||
|
||||
#container {
|
||||
width: 800px;
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.c1 {
|
||||
width: 800px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.c2 {
|
||||
width: 355px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.c3 {
|
||||
width: 235px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem
|
||||
}
|
||||
|
||||
h1 {
|
||||
letter-spacing: 1px;
|
||||
text-align: center;
|
||||
text-shadow: #262729 0 -1px 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #bbb;
|
||||
text-shadow: #262729 0 -1px 0;
|
||||
font-weight: 300;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
input {
|
||||
padding: 2px 10px;
|
||||
border-radius: 10px;
|
||||
border: solid 1px #555;
|
||||
float: left;
|
||||
margin-right: 10px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
border: none;
|
||||
outline: none;
|
||||
color: #007bff;
|
||||
font-weight: bold;
|
||||
border-top: solid 1px #aaa;
|
||||
border-right: solid 1px #e6e6e6;
|
||||
border-bottom: solid 1px #e6e6e6;
|
||||
border-left: solid 1px #aaa;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: rgba(23, 25, 28, .8);
|
||||
border: solid 1px rgba(255, 255, 255, .1);
|
||||
padding: 10px 10px;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
color: #ddd;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
tr:hover {
|
||||
background-color: #454c54;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 10px 10px;
|
||||
border: solid 1px rgba(255, 255, 255, .1);
|
||||
width: auto;
|
||||
}
|
||||
|
||||
td.update,
|
||||
td.remove,
|
||||
td.type {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
td.address {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
button.add-btn,
|
||||
button.update-btn,
|
||||
button.cancel-btn,
|
||||
button.edit-item-btn,
|
||||
button.remove-item-btn {
|
||||
font-size: 1.33rem;
|
||||
border: 2px #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
button.save-btn,
|
||||
button.export-btn,
|
||||
button.import-btn,
|
||||
button.restart-btn {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
#container.mapping-page {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
#mapping {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
#mapping td.update,
|
||||
#mapping td.remove {
|
||||
width: 130px;
|
||||
}
|
||||
|
||||
#mapping td.add {
|
||||
width: 300px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#mapping td.foot {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#mapping input {
|
||||
width: 130px;
|
||||
margin: 0;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
#mapping input.search {
|
||||
width: 185px;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
if (isset($_POST['save-state'])) {
|
||||
$output = shell_exec('/usr/bin/supervisorctl -c /etc/supervisor/logstash/supervisord.conf restart logstash');
|
||||
echo "<pre>$output</pre>";
|
||||
}
|
||||
?>
|
||||
8
Vagrant/resources/malcolm/name-map-ui/site/upload.html
Normal file
8
Vagrant/resources/malcolm/name-map-ui/site/upload.html
Normal file
@@ -0,0 +1,8 @@
|
||||
<html>
|
||||
<body>
|
||||
<form enctype="multipart/form-data" action="upload.php" method="post">
|
||||
Choose a file to upload: <input name="upfile" type="file" />
|
||||
<input type="submit" value="Upload" />
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
57
Vagrant/resources/malcolm/name-map-ui/site/upload.php
Normal file
57
Vagrant/resources/malcolm/name-map-ui/site/upload.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
header('Content-Type: text/plain; charset=utf-8');
|
||||
|
||||
try {
|
||||
|
||||
// invalid multiple files / $_FILES corruption attack
|
||||
if (!isset($_FILES['upfile']['error']) ||
|
||||
is_array($_FILES['upfile']['error'])) {
|
||||
throw new RuntimeException('Invalid parameters');
|
||||
}
|
||||
|
||||
// validate $_FILES['upfile']['error']
|
||||
switch ($_FILES['upfile']['error']) {
|
||||
case UPLOAD_ERR_OK:
|
||||
break;
|
||||
case UPLOAD_ERR_NO_FILE:
|
||||
throw new RuntimeException('No file sent');
|
||||
case UPLOAD_ERR_INI_SIZE:
|
||||
case UPLOAD_ERR_FORM_SIZE:
|
||||
throw new RuntimeException('Exceeded filesize limit');
|
||||
default:
|
||||
throw new RuntimeException('Unknown error');
|
||||
}
|
||||
|
||||
// maximum upload filesize
|
||||
if ($_FILES['upfile']['size'] > 67108864) {
|
||||
throw new RuntimeException('Exceeded filesize limit');
|
||||
}
|
||||
|
||||
// check upload MIME type
|
||||
$finfo = new finfo(FILEINFO_MIME_TYPE);
|
||||
$fmime = $finfo->file($_FILES['upfile']['tmp_name']);
|
||||
if (false === $ext = array_search($fmime,
|
||||
array('json' => 'application/json',
|
||||
'txt' => 'text/plain'),
|
||||
true)) {
|
||||
throw new RuntimeException(sprintf('Invalid file format: "%s"', $fmime));
|
||||
}
|
||||
|
||||
// give file unique name based on sha
|
||||
$ftmpname = $_FILES['upfile']['tmp_name'];
|
||||
$fdstname = sprintf('./upload/%s.%s',
|
||||
sha1_file($_FILES['upfile']['tmp_name']),
|
||||
$ext);
|
||||
if (!move_uploaded_file($ftmpname, $fdstname)) {
|
||||
throw new RuntimeException(sprintf('Failed to move uploaded file ("%s" -> "%s")', $ftmpname, $fdstname));
|
||||
}
|
||||
|
||||
echo 'Success';
|
||||
|
||||
} catch (RuntimeException $e) {
|
||||
error_log ($e->getMessage());
|
||||
echo $e->getMessage();
|
||||
}
|
||||
|
||||
?>
|
||||
Reference in New Issue
Block a user