added multi agent execution
This commit is contained in:
parent
2056479224
commit
1ce6d2e676
18
main.go
18
main.go
|
@ -6,7 +6,7 @@ import (
|
|||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"slices"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"html/template"
|
||||
|
@ -118,17 +118,11 @@ func listAgents(w http.ResponseWriter, r *http.Request) {
|
|||
agents, err := api.GetAgents(db)
|
||||
currentAgents := getAgentsStatus()
|
||||
|
||||
for _, currAgent := range currentAgents {
|
||||
for i, agent := range agents {
|
||||
if currAgent == agent.AgentName {
|
||||
// log.Printf("%s online", agent.AgentName)
|
||||
// logger.InsertLog(logger.Debug, fmt.Sprintf("%s online after page refresh", agent.AgentName))
|
||||
// agents[i].Status = fmt.Sprint("<span class=\"badge bg-success\">Connected</span>")
|
||||
agents[i].Status = "Connected"
|
||||
} else {
|
||||
// agent.Status = fmt.Sprintf("<span class=\"badge bg-danger\">Disconnected</span>")
|
||||
agents[i].Status = "Disconnected"
|
||||
}
|
||||
for i := range agents {
|
||||
if slices.Contains(currentAgents, agents[i].AgentName) {
|
||||
agents[i].Status = "Connected"
|
||||
} else {
|
||||
agents[i].Status = "Disconnected"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -235,6 +235,67 @@ type Message struct {
|
|||
Payload string `json:"payload"`
|
||||
}
|
||||
|
||||
// var executeCommand http.HandlerFunc = func(w http.ResponseWriter, r *http.Request){
|
||||
// err := r.ParseForm()
|
||||
// if err != nil {
|
||||
// http.Error(w, "Invalid form data", http.StatusBadRequest)
|
||||
// logger.InsertLog(logger.Info, "Invalid form data")
|
||||
// return
|
||||
// }
|
||||
|
||||
// agentName := r.FormValue("agentName")
|
||||
// command := r.FormValue("command")
|
||||
|
||||
// agentSocketsMutex.Lock()
|
||||
// conn, ok := agentSockets[agentName]
|
||||
// agentSocketsMutex.Unlock()
|
||||
|
||||
// if !ok {
|
||||
// http.Error(w, "Agent not connected", http.StatusNotFound)
|
||||
// logger.InsertLog(logger.Info, "Agent not connected")
|
||||
// return
|
||||
// }
|
||||
|
||||
// responseChan := make(chan string, 1)
|
||||
// responseChannels.Store(agentName, responseChan)
|
||||
// defer responseChannels.Delete(agentName)
|
||||
|
||||
// message := Message {
|
||||
// Type: "command",
|
||||
// Payload: command,
|
||||
// }
|
||||
|
||||
// messageBytes, _ := json.Marshal(message)
|
||||
|
||||
// err = conn.WriteMessage(websocket.TextMessage, messageBytes)
|
||||
// if err != nil {
|
||||
// http.Error(w, "Failed to send command to the agent", http.StatusInternalServerError)
|
||||
// logger.InsertLog(logger.Error, "Failed to send command to the agent")
|
||||
// return
|
||||
// }
|
||||
|
||||
// select {
|
||||
// case response := <-responseChan:
|
||||
// var parsedResponse map[string]string
|
||||
// if err := json.Unmarshal([]byte(response), &parsedResponse); err != nil {
|
||||
// http.Error(w, "Failed to parse response", http.StatusInternalServerError)
|
||||
// return
|
||||
// }
|
||||
// payload, ok := parsedResponse["payload"]
|
||||
// if !ok {
|
||||
// http.Error(w, "Invalid response structure", http.StatusInternalServerError)
|
||||
// logger.InsertLog(logger.Error, "Invalid response structure")
|
||||
// return
|
||||
// }
|
||||
// w.WriteHeader(http.StatusOK)
|
||||
// w.Header().Set("Content-Type", "text/plain")
|
||||
// w.Write([]byte(payload))
|
||||
// case <- time.After(10 * time.Second):
|
||||
// http.Error(w, "Agent response timed out", http.StatusGatewayTimeout)
|
||||
// logger.InsertLog(logger.Info, "Agent response timed out")
|
||||
// }
|
||||
// }
|
||||
|
||||
var executeCommand http.HandlerFunc = func(w http.ResponseWriter, r *http.Request){
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
|
@ -243,59 +304,103 @@ var executeCommand http.HandlerFunc = func(w http.ResponseWriter, r *http.Reques
|
|||
return
|
||||
}
|
||||
|
||||
agentName := r.FormValue("agentName")
|
||||
agentNameStr := r.FormValue("agentNames")
|
||||
var agentNames []string
|
||||
|
||||
if agentNameStr != "" {
|
||||
agentNames = strings.Split(agentNameStr, ",")
|
||||
} else {
|
||||
agentName := r.FormValue("agentName")
|
||||
if agentName != "" {
|
||||
agentNames = []string{agentName}
|
||||
}
|
||||
}
|
||||
|
||||
command := r.FormValue("command")
|
||||
|
||||
agentSocketsMutex.Lock()
|
||||
conn, ok := agentSockets[agentName]
|
||||
agentSocketsMutex.Unlock()
|
||||
|
||||
if !ok {
|
||||
http.Error(w, "Agent not connected", http.StatusNotFound)
|
||||
logger.InsertLog(logger.Info, "Agent not connected")
|
||||
if len(agentNames) == 0 || command == "" {
|
||||
http.Error(w, "Missing agent or command", http.StatusBadRequest)
|
||||
logger.InsertLog(logger.Error, "Missing agent or command")
|
||||
return
|
||||
}
|
||||
|
||||
responseChan := make(chan string, 1)
|
||||
responseChannels.Store(agentName, responseChan)
|
||||
defer responseChannels.Delete(agentName)
|
||||
|
||||
message := Message {
|
||||
Type: "command",
|
||||
Payload: command,
|
||||
type result struct {
|
||||
AgentName string
|
||||
Type string
|
||||
Payload string
|
||||
Err error
|
||||
}
|
||||
|
||||
messageBytes, _ := json.Marshal(message)
|
||||
resultsChan := make(chan result, len(agentNames))
|
||||
|
||||
err = conn.WriteMessage(websocket.TextMessage, messageBytes)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to send command to the agent", http.StatusInternalServerError)
|
||||
logger.InsertLog(logger.Error, "Failed to send command to the agent")
|
||||
return
|
||||
for _, agentName := range agentNames {
|
||||
agentName := strings.TrimSpace(agentName)
|
||||
|
||||
go func(agent string) {
|
||||
agentSocketsMutex.Lock()
|
||||
conn, ok := agentSockets[agentName]
|
||||
agentSocketsMutex.Unlock()
|
||||
|
||||
if !ok {
|
||||
resultsChan <- result{AgentName: agent, Err: fmt.Errorf("Agent not connected")}
|
||||
return
|
||||
}
|
||||
|
||||
responseChan := make(chan string, 1)
|
||||
responseChannels.Store(agent, responseChan)
|
||||
defer responseChannels.Delete(agent)
|
||||
|
||||
msg := Message {
|
||||
Type: "command",
|
||||
Payload: command,
|
||||
}
|
||||
|
||||
msgBytes, _ := json.Marshal(msg)
|
||||
err := conn.WriteMessage(websocket.TextMessage, msgBytes)
|
||||
if err != nil {
|
||||
resultsChan <- result{AgentName: agent, Err: fmt.Errorf("Send failed")}
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case resp := <- responseChan:
|
||||
var parsed map[string]string
|
||||
if err:= json.Unmarshal([]byte(resp), &parsed); err != nil {
|
||||
resultsChan <- result{AgentName: agent, Err: fmt.Errorf("Invalid response")}
|
||||
return
|
||||
}
|
||||
|
||||
payload, ok := parsed["payload"]
|
||||
if !ok {
|
||||
resultsChan <- result{AgentName: agent, Err: fmt.Errorf("No payload")}
|
||||
return
|
||||
}
|
||||
|
||||
resultsChan <- result{AgentName: agent, Payload: payload}
|
||||
case <-time.After(10 * time.Second):
|
||||
resultsChan <- result{AgentName: agent, Err: fmt.Errorf("Timeout")}
|
||||
}
|
||||
} (agentName)
|
||||
}
|
||||
|
||||
select {
|
||||
case response := <-responseChan:
|
||||
var parsedResponse map[string]string
|
||||
if err := json.Unmarshal([]byte(response), &parsedResponse); err != nil {
|
||||
http.Error(w, "Failed to parse response", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
payload, ok := parsedResponse["payload"]
|
||||
if !ok {
|
||||
http.Error(w, "Invalid response structure", http.StatusInternalServerError)
|
||||
logger.InsertLog(logger.Error, "Invalid response structure")
|
||||
return
|
||||
var combined strings.Builder
|
||||
for i := 0; i < len(agentNames); i++ {
|
||||
res := <- resultsChan
|
||||
if res.Err != nil {
|
||||
combined.WriteString(fmt.Sprintf("[%s] ERROR: %s\n", res.AgentName, res.Err.Error()))
|
||||
} else {
|
||||
combined.WriteString(fmt.Sprintf("[%s] %s\n", res.AgentName, res.Payload))
|
||||
}
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.Write([]byte(payload))
|
||||
case <- time.After(10 * time.Second):
|
||||
http.Error(w, "Agent response timed out", http.StatusGatewayTimeout)
|
||||
logger.InsertLog(logger.Info, "Agent response timed out")
|
||||
}
|
||||
w.Write([]byte(combined.String()))
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
func Server() (*http.Server) {
|
||||
webSocketHandler := webSocketHandler {
|
||||
upgrader: websocket.Upgrader{
|
||||
|
|
|
@ -10,101 +10,84 @@
|
|||
<!-- <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script> -->
|
||||
<title>g2: gommand & gontrol</title>
|
||||
<script>
|
||||
// Query Agents for the Dropdown Menu
|
||||
<!-- document.addEventListener('DOMContentLoaded', () => { -->
|
||||
<!-- fetch('/agentNames') -->
|
||||
<!-- .then(response => response.json()) -->
|
||||
<!-- .then(agentNames => { -->
|
||||
<!-- const dropdown = document.getElementById('agentName'); -->
|
||||
<!-- agentNames.forEach(name => { -->
|
||||
<!-- const option = document.createElement('option'); -->
|
||||
<!-- option.value = name; -->
|
||||
<!-- option.textContent = name; -->
|
||||
<!-- dropdown.appendChild(option); -->
|
||||
<!-- }); -->
|
||||
<!-- }) -->
|
||||
<!-- .catch(error => console.error('Error fetching agent names:', error)); -->
|
||||
<!-- }); -->
|
||||
<!-- // Query agents currently connected to the websocket and put status into the table -->
|
||||
<!-- const updateAgentStatuses = () => { -->
|
||||
<!-- fetch('http://localhost:5555/agentNames') -->
|
||||
<!-- .then(response => response.json()) -->
|
||||
<!-- .then(agentNames => { -->
|
||||
<!-- console.log("Agent names fetched:", agentNames); -->
|
||||
<!-- const tableRows = document.querySelectorAll('#agentList table tbody tr'); -->
|
||||
<!-- tableRows.forEach(row => { -->
|
||||
<!-- const nameCell = row.querySelector('td:nth-child(2)'); -->
|
||||
<!-- const statusCell = row.querySelector('td:nth-child(5)'); -->
|
||||
<!-- if (nameCell && statusCell) { -->
|
||||
<!-- const agentName = nameCell.textContent.trim(); -->
|
||||
<!-- if (agentNames.includes(agentName)) { -->
|
||||
<!-- statusCell.innerHTML = '<span class="badge bg-success">Connected</span>'; -->
|
||||
<!-- } else { -->
|
||||
<!-- statusCell.innerHTML = '<span class="badge bg-danger">Disconnected</span>'; -->
|
||||
<!-- } -->
|
||||
<!-- } -->
|
||||
<!-- }); -->
|
||||
<!-- }) -->
|
||||
<!-- .catch(error => console.error('Error fetching agent names:', error)); -->
|
||||
<!-- }; -->
|
||||
<!-- updateAgentStatuses(); -->
|
||||
<!-- setInterval(updateAgentStatuses, 5000); -->
|
||||
|
||||
<!-- document.body.addEventListener('htmx:afterSwap', function(evt) { -->
|
||||
<!-- if (evt.detail.xhr.status === 200) { -->
|
||||
<!-- const tableRows = document.querySelectorAll('#agentList table tbody tr'); -->
|
||||
<!-- tableRows.forEach(row => { -->
|
||||
<!-- const nameCell = row.querySelector('td:nth-child(2)'); -->
|
||||
<!-- const statusCell = row.querySelector('td:nth-child(5)'); -->
|
||||
<!-- if (nameCell && statusCell) { -->
|
||||
<!-- const agentName = nameCell.textContent.trim(); -->
|
||||
<!-- if ("Connected" === statusCell.innerHTML) { -->
|
||||
<!-- statusCell.innerHTML = '<span class="badge bg-success">Connected</span>'; -->
|
||||
<!-- } else { -->
|
||||
<!-- statusCell.innerHTML = '<span class="badge bg-danger">Disconnected</span>'; -->
|
||||
<!-- } -->
|
||||
<!-- } -->
|
||||
<!-- } -->
|
||||
<!-- } -->
|
||||
<!-- }); -->
|
||||
const checkboxState = new Map();
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document.body.addEventListener('htmx:afterSwap', function(event) {
|
||||
|
||||
document.body.addEventListener('htmx:beforeSwap', function(event) {
|
||||
if (event.target.id === "agentList") {
|
||||
updateAgentDropdown();
|
||||
saveCheckboxState();
|
||||
}
|
||||
});
|
||||
|
||||
function updateAgentDropdown() {
|
||||
const select = document.getElementById("agentName");
|
||||
const optionValues = Array.from(select.options).map(opt => opt.value);
|
||||
const rows = document.querySelectorAll("#agentList tbody tr");
|
||||
|
||||
rows.forEach(row => {
|
||||
const status = row.cells[4].textContent.trim();
|
||||
const name = row.cells[1].textContent.trim();
|
||||
|
||||
if (status === "Connected") {
|
||||
row.cells[4].innerHTML = '<span class="badge bg-success">Connected</span>';
|
||||
const option = document.createElement("option");
|
||||
if (!(optionValues.includes(name))) {
|
||||
option.value = name;
|
||||
option.textContent = name;
|
||||
select.appendChild(option);
|
||||
}
|
||||
}
|
||||
|
||||
if (status === "Disconnected") {
|
||||
row.cells[4].innerHTML = '<span class="badge bg-danger">Disconnected</span>';
|
||||
const option = Array.from(select.options).find(opt => opt.value === name);
|
||||
if(option) {
|
||||
select.removeChild(option);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
document.body.addEventListener('htmx:afterSwap', function(event) {
|
||||
if (event.target.id === "agentList") {
|
||||
restoreCheckboxState();
|
||||
updateAgentDropdown();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function prepareAgentNames(event) {
|
||||
const selected = Array.from(document.querySelectorAll('.agent-checkbox'))
|
||||
.filter(cb => cb.checked)
|
||||
.map(cb => cb.dataset.agentName);
|
||||
|
||||
const hiddenInput = document.getElementById('agentNamesInput');
|
||||
|
||||
if (selected.length > 0) {
|
||||
document.getElementById('agentName').removeAttribute('name');
|
||||
hiddenInput.value = selected.join(',');
|
||||
} else {
|
||||
document.getElementById('agentName').setAttribute('name', 'agentName');
|
||||
hiddenInput.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
function saveCheckboxState() {
|
||||
document.querySelectorAll('.agent-checkbox').forEach((checkbox) => {
|
||||
checkboxState.set(checkbox.dataset.agentName, checkbox.checked);
|
||||
});
|
||||
}
|
||||
|
||||
function restoreCheckboxState() {
|
||||
document.querySelectorAll('.agent-checkbox').forEach((checkbox) => {
|
||||
const state = checkboxState.get(checkbox.dataset.agentName);
|
||||
if (state !== undefined) {
|
||||
checkbox.checked = state;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateAgentDropdown() {
|
||||
const select = document.getElementById("agentName");
|
||||
const optionValues = Array.from(select.options).map(opt => opt.value);
|
||||
const rows = document.querySelectorAll("#agentList tbody tr");
|
||||
|
||||
rows.forEach(row => {
|
||||
const status = row.cells[4].textContent.trim();
|
||||
const name = row.cells[1].textContent.trim();
|
||||
|
||||
if (status === "Connected") {
|
||||
row.cells[4].innerHTML = '<span class="badge bg-success">Connected</span>';
|
||||
const option = document.createElement("option");
|
||||
if (!(optionValues.includes(name))) {
|
||||
option.value = name;
|
||||
option.textContent = name;
|
||||
select.appendChild(option);
|
||||
}
|
||||
}
|
||||
|
||||
if (status === "Disconnected") {
|
||||
row.cells[4].innerHTML = '<span class="badge bg-danger">Disconnected</span>';
|
||||
const option = Array.from(select.options).find(opt => opt.value === name);
|
||||
if(option) {
|
||||
select.removeChild(option);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
</script>
|
||||
<style>
|
||||
.log-info {
|
||||
|
@ -137,11 +120,11 @@
|
|||
<!-- Agent Commands -->
|
||||
<div id="agentCommands">
|
||||
<h3>Command Execution</h3>
|
||||
<form hx-post="http://localhost:5555/executeCommand" hx-target="#commandOutput" hx-encoding="application/x-www-form-urlencoded" hx-swap="innerHTML">
|
||||
<form hx-post="http://localhost:5555/executeCommand" hx-target="#commandOutput" hx-encoding="application/x-www-form-urlencoded" hx-swap="innerHTML" onsubmit="prepareAgentNames(event)">
|
||||
<div class="mb-3">
|
||||
<label for="agentName" class="form-label">Agent Name</label>
|
||||
<!-- <select class="form-select" id="agentName" name="agentName" required> -->
|
||||
<select id="agentName" class="form-select" name="agentName" hx-on="htmx:afterSwap:updateAgentDropdown" required>
|
||||
<select id="agentName" class="form-select" name="agentName" hx-on="htmx:afterSwap:updateAgentDropdown">
|
||||
<option value="" disabled selected>Select an Agent</option>
|
||||
<!-- Dynamically populated with agent names -->
|
||||
</select>
|
||||
|
@ -151,39 +134,12 @@
|
|||
<input type="text" class="form-control" id="command" name="command" placeholder="Enter command" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Execute</button>
|
||||
<!-- Hidden checkbox form !-->
|
||||
<input type="hidden" name="agentNames" id="agentNamesInput">
|
||||
</form>
|
||||
<pre id="commandOutput"></pre>
|
||||
</div>
|
||||
|
||||
<!-- Add Agent Form -->
|
||||
<!-- <button class="btn btn-primary mt-3" data-bs-toggle="collapse" data-bs-target="#addAgentForm">Add Agent</button> -->
|
||||
<!-- <div id="addAgentForm" class="collapse mt-2"> -->
|
||||
<!-- <form hx-post="/agents" hx-target="#agentList" hx-swap="innerHTML"> -->
|
||||
<!-- <div class="mb-3"> -->
|
||||
<!-- <label for="agentId" class="form-label">Agent Id</label> -->
|
||||
<!-- <input type="text" class="form-control" id="agentId" name="agentId" required> -->
|
||||
<!-- </div> -->
|
||||
<!-- <div class="mb-3"> -->
|
||||
<!-- <label for="agentName" class="form-label">Agent Name</label> -->
|
||||
<!-- <input type="text" class="form-control" id="agentName" name="agentName" required> -->
|
||||
<!-- </div> -->
|
||||
<!-- <div class="mb-3"> -->
|
||||
<!-- <label for="IPv4Address" class="form-label">IPv4 Address</label> -->
|
||||
<!-- <input type="text" class="form-control" id="IPv4Address" name="IPv4Address" required> -->
|
||||
<!-- </div> -->
|
||||
<!-- <div class="mb-3"> -->
|
||||
<!-- <label for="initialContact" class="form-label">Initial Contact</label> -->
|
||||
<!-- <input type="datetime-local" class="form-control" id="initialContact" name="initialContact" required> -->
|
||||
<!-- </div> -->
|
||||
<!-- <div class="mb-3"> -->
|
||||
<!-- <label for="lastContact" class="form-label">Last Contact</label> -->
|
||||
<!-- <input type="datetime-local" class="form-control" id="lastContact" name="lastContact" required> -->
|
||||
<!-- </div> -->
|
||||
<!-- <button type="submit" class="btn btn-success">Add Agent</button> -->
|
||||
<!-- </form> -->
|
||||
<!-- </div> -->
|
||||
<!-- </div> -->
|
||||
|
||||
<!-- Logs Section -->
|
||||
<h3>Logs</h3>
|
||||
<div id="logs-container" hx-get="/logs" hx-target="#logs-container" hx-swap="innerHTML" hx-trigger="every 3s">
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
<!-- <td><span class="badge bg-danger">Disconnected</span></td> -->
|
||||
<td>{{.Status}}</td>
|
||||
<td>
|
||||
<input type="checkbox" class="agent-checkbox" data-agent-name="{{.AgentName}}">
|
||||
<button class="btn btn-warning" hx-get="/agents/{{.AgentID}}" hx-target="#agentDetails" hx-swap="innerHTML">View</button>
|
||||
|
||||
<button class="btn btn-danger" hx-delete="/agents/{{.AgentID}}" hx-target="#agentList" hx-swap="innerHTML">Delete</button>
|
||||
|
|
Loading…
Reference in New Issue