From cb02c59057fbe098ca6db23ca0ca167c30fe637b Mon Sep 17 00:00:00 2001 From: Ralf Date: Fri, 29 Nov 2024 20:25:32 +0300 Subject: [PATCH] Add Browser - disk --- AdaptixServer/core/connector/connector.go | 3 + AdaptixServer/core/connector/tc_browsers.go | 42 +++++ AdaptixServer/core/extender/ex_agent.go | 12 ++ AdaptixServer/core/extender/extender.go | 4 + AdaptixServer/core/extender/utils.go | 2 + AdaptixServer/core/server/ts_browser.go | 97 +++++++++++ AdaptixServer/core/server/ts_syncpacket.go | 17 ++ AdaptixServer/core/server/ts_tasks.go | 21 ++- AdaptixServer/core/server/utils.go | 37 ++-- .../extenders/agent_beacon/pl_agent.go | 14 +- .../extenders/agent_beacon/pl_main.go | 85 +++++++-- Client/CMakeLists.txt | 2 + Client/Headers/Agent/Agent.h | 9 +- Client/Headers/Client/Requestor.h | 2 + Client/Headers/UI/Widgets/AdaptixWidget.h | 1 + .../Headers/UI/Widgets/BrowserFilesWidget.h | 48 ++++++ Client/Headers/UI/Widgets/ConsoleWidget.h | 2 +- .../Headers/UI/Widgets/SessionsTableWidget.h | 1 + Client/Headers/Utils/FileSystem.h | 2 + Client/Headers/main.h | 3 + Client/Resources/Resources.qrc | 3 +- .../Resources/icons/arrow_right_alt_64dp.png | Bin 0 -> 3306 bytes Client/Resources/icons/autorenew_64dp.png | Bin 0 -> 9254 bytes Client/Resources/icons/folder_64dp.png | Bin 0 -> 3592 bytes Client/Resources/icons/folder_open_64dp.png | Bin 0 -> 5823 bytes Client/Resources/icons/reply_64dp.png | Bin 0 -> 4441 bytes Client/Resources/icons/storage_64dp.png | Bin 0 -> 2396 bytes Client/Resources/icons/upload_file_64dp.png | Bin 0 -> 4541 bytes Client/Source/Agent/Agent.cpp | 20 ++- Client/Source/Client/ProcessSyncPacket.cpp | 35 +++- Client/Source/Client/Requestor.cpp | 16 ++ Client/Source/UI/Widgets/AdaptixWidget.cpp | 9 + .../Source/UI/Widgets/BrowserFilesWidget.cpp | 163 ++++++++++++++++++ Client/Source/UI/Widgets/ConsoleWidget.cpp | 2 +- .../Source/UI/Widgets/SessionsTableWidget.cpp | 30 +++- Client/Source/Utils/FileSystem.cpp | 17 ++ 36 files changed, 649 insertions(+), 50 deletions(-) create mode 100644 AdaptixServer/core/server/ts_browser.go create mode 100644 Client/Headers/UI/Widgets/BrowserFilesWidget.h create mode 100644 Client/Resources/icons/arrow_right_alt_64dp.png create mode 100644 Client/Resources/icons/autorenew_64dp.png create mode 100644 Client/Resources/icons/folder_64dp.png create mode 100644 Client/Resources/icons/folder_open_64dp.png create mode 100644 Client/Resources/icons/reply_64dp.png create mode 100644 Client/Resources/icons/storage_64dp.png create mode 100644 Client/Resources/icons/upload_file_64dp.png create mode 100644 Client/Source/UI/Widgets/BrowserFilesWidget.cpp diff --git a/AdaptixServer/core/connector/connector.go b/AdaptixServer/core/connector/connector.go index 2f8d613d..b219bdf4 100644 --- a/AdaptixServer/core/connector/connector.go +++ b/AdaptixServer/core/connector/connector.go @@ -36,7 +36,9 @@ type Teamserver interface { TsDownloadClose(fileId string, reason int) error TsDownloadSync(fileId string) (string, []byte, error) TsDownloadDelete(fileId string) error + TsDownloadChangeState(fileId string, username string, command string) error + TsAgentBrowserDisks(agentId string, username string) error } type TsConnector struct { @@ -92,6 +94,7 @@ func NewTsConnector(ts Teamserver, p profile.TsProfile) (*TsConnector, error) { connector.Engine.POST(p.Endpoint+"/agent/settag", token.ValidateAccessToken(), default404Middleware(), connector.TcAgentSetTag) connector.Engine.POST(p.Endpoint+"/browser/download", token.ValidateAccessToken(), default404Middleware(), connector.TcBrowserDownload) + connector.Engine.POST(p.Endpoint+"/browser/disks", token.ValidateAccessToken(), default404Middleware(), connector.TcBrowserDisks) connector.Engine.NoRoute(default404Middleware(), func(c *gin.Context) { _ = c.Error(errors.New("NoRoute")) }) diff --git a/AdaptixServer/core/connector/tc_browsers.go b/AdaptixServer/core/connector/tc_browsers.go index 96f8aa2f..4624651a 100644 --- a/AdaptixServer/core/connector/tc_browsers.go +++ b/AdaptixServer/core/connector/tc_browsers.go @@ -76,3 +76,45 @@ func (tc *TsConnector) TcBrowserDownload(ctx *gin.Context) { ctx.JSON(http.StatusOK, answer) } + +/// + +type DisksAction struct { + AgentId string `json:"agent_id"` +} + +func (tc *TsConnector) TcBrowserDisks(ctx *gin.Context) { + var ( + disksAction DisksAction + username string + ok bool + err error + answer gin.H + ) + + err = ctx.ShouldBindJSON(&disksAction) + if err != nil { + _ = ctx.Error(errors.New("invalid action")) + return + } + + value, exists := ctx.Get("username") + if !exists { + ctx.JSON(http.StatusOK, gin.H{"message": "Server error: username not found in context", "ok": false}) + return + } + + username, ok = value.(string) + if !ok { + ctx.JSON(http.StatusOK, gin.H{"message": "Server error: invalid username type in context", "ok": false}) + return + } + + err = tc.teamserver.TsAgentBrowserDisks(disksAction.AgentId, username) + if err != nil { + answer = gin.H{"message": err.Error(), "ok": false} + } else { + answer = gin.H{"message": "Wait...", "ok": true} + } + ctx.JSON(http.StatusOK, answer) +} diff --git a/AdaptixServer/core/extender/ex_agent.go b/AdaptixServer/core/extender/ex_agent.go index 570418d9..6e497a8c 100644 --- a/AdaptixServer/core/extender/ex_agent.go +++ b/AdaptixServer/core/extender/ex_agent.go @@ -61,3 +61,15 @@ func (ex *AdaptixExtender) ExAgentDownloadChangeState(agentName string, agentObj return nil, errors.New("module not found") } } + +func (ex *AdaptixExtender) ExAgentBrowserDisks(agentName string, agentObject []byte) ([]byte, error) { + var module *ModuleExtender + + value, ok := ex.agentModules.Get(agentName) + if ok { + module = value.(*ModuleExtender) + return module.AgentBrowserDisks(agentObject) + } else { + return nil, errors.New("module not found") + } +} diff --git a/AdaptixServer/core/extender/extender.go b/AdaptixServer/core/extender/extender.go index 30826779..8b84ebc5 100644 --- a/AdaptixServer/core/extender/extender.go +++ b/AdaptixServer/core/extender/extender.go @@ -141,6 +141,10 @@ func (ex *AdaptixExtender) ValidPlugin(info ModuleInfo, object plugin.Symbol) er if !ok { return errors.New("method AgentDownloadChangeState not found") } + _, ok = reflect.TypeOf(object).MethodByName("AgentBrowserDisks") + if !ok { + return errors.New("method AgentBrowserDisks not found") + } return nil } diff --git a/AdaptixServer/core/extender/utils.go b/AdaptixServer/core/extender/utils.go index 5cd0c9a0..02d9992b 100644 --- a/AdaptixServer/core/extender/utils.go +++ b/AdaptixServer/core/extender/utils.go @@ -49,7 +49,9 @@ type AgentFunctions interface { AgentProcessData(agentObject []byte, packedData []byte) ([]byte, error) AgentPackData(agentObject []byte, dataTasks [][]byte) ([]byte, error) AgentCommand(agentObject []byte, args map[string]any) ([]byte, string, error) + AgentDownloadChangeState(agentObject []byte, newState int, fileId string) ([]byte, error) + AgentBrowserDisks(agentObject []byte) ([]byte, error) } type ModuleExtender struct { diff --git a/AdaptixServer/core/server/ts_browser.go b/AdaptixServer/core/server/ts_browser.go new file mode 100644 index 00000000..9bd1f822 --- /dev/null +++ b/AdaptixServer/core/server/ts_browser.go @@ -0,0 +1,97 @@ +package server + +import ( + "AdaptixServer/core/utils/krypt" + "bytes" + "encoding/json" + "fmt" + "time" +) + +/// AGENT + +func (ts *Teamserver) TsAgentBrowserDisks(agentId string, username string) error { + var ( + err error + agentObject bytes.Buffer + agent *Agent + taskData TaskData + data []byte + ) + + value, ok := ts.agents.Get(agentId) + if ok { + + agent, _ = value.(*Agent) + _ = json.NewEncoder(&agentObject).Encode(agent.Data) + + data, err = ts.Extender.ExAgentBrowserDisks(agent.Data.Name, agentObject.Bytes()) + if err != nil { + return err + } + + err = json.Unmarshal(data, &taskData) + if err != nil { + return err + } + + if taskData.TaskId == "" { + taskData.TaskId, _ = krypt.GenerateUID(8) + } + taskData.AgentId = agentId + taskData.User = username + taskData.StartDate = time.Now().Unix() + + agent.TasksQueue.Put(taskData) + + } else { + return fmt.Errorf("agent '%v' does not exist", agentId) + } + + return nil +} + +/// SYNC + +func (ts *Teamserver) TsClientBrowserDisks(jsonTask string, jsonDrives string) { + var ( + agent *Agent + task TaskData + taskData TaskData + value any + ok bool + err error + ) + + err = json.Unmarshal([]byte(jsonTask), &taskData) + if err != nil { + return + } + + value, ok = ts.agents.Get(taskData.AgentId) + if ok { + agent = value.(*Agent) + } else { + return + } + + value, ok = agent.Tasks.Get(taskData.TaskId) + if ok { + task = value.(TaskData) + } else { + return + } + + if task.Type != TYPE_BROWSER { + return + } + + agent.Tasks.Delete(taskData.TaskId) + + if taskData.MessageType != CONSOLE_OUT_ERROR && taskData.MessageType != CONSOLE_OUT_LOCAL_ERROR { + taskData.Message = "Status: OK" + } + + packet := CreateSpBrowserDisks(taskData, jsonDrives) + ts.TsSyncClient(task.User, packet) +} diff --git a/AdaptixServer/core/server/ts_syncpacket.go b/AdaptixServer/core/server/ts_syncpacket.go index ac88f3d8..78034307 100644 --- a/AdaptixServer/core/server/ts_syncpacket.go +++ b/AdaptixServer/core/server/ts_syncpacket.go @@ -32,6 +32,8 @@ const ( TYPE_DOWNLOAD_CREATE = 0x51 TYPE_DOWNLOAD_UPDATE = 0x52 TYPE_DOWNLOAD_DELETE = 0x53 + + TYPE_BROWSER_DISKS = 0x61 ) /// SYNC @@ -274,3 +276,18 @@ func CreateSpDownloadDelete(fileId string) SyncPackerDownloadDelete { FileId: fileId, } } + +/// BROWSER + +func CreateSpBrowserDisks(taskData TaskData, data string) SyncPacketBrowserDisks { + return SyncPacketBrowserDisks{ + store: STORE_LOG, + SpType: TYPE_BROWSER_DISKS, + + AgentId: taskData.AgentId, + Time: time.Now().UTC().Unix(), + MessageType: taskData.MessageType, + Message: taskData.Message, + Data: data, + } +} diff --git a/AdaptixServer/core/server/ts_tasks.go b/AdaptixServer/core/server/ts_tasks.go index b9d19e59..0b36d614 100644 --- a/AdaptixServer/core/server/ts_tasks.go +++ b/AdaptixServer/core/server/ts_tasks.go @@ -93,19 +93,18 @@ func (ts *Teamserver) TsTaskUpdate(agentId string, taskObject []byte) { } value, ok = agent.Tasks.GetDelete(taskData.TaskId) - if ok { - task = value.(TaskData) - task.Data = []byte("") - task.FinishDate = taskData.FinishDate - task.MessageType = taskData.MessageType - task.Message = taskData.Message - task.ClearText = taskData.ClearText - task.Completed = taskData.Completed - } else { - task = taskData - logs.Error("TsTaskUpdate: task %v not found", taskData.TaskId) + if !ok { + return } + task = value.(TaskData) + task.Data = []byte("") + task.FinishDate = taskData.FinishDate + task.MessageType = taskData.MessageType + task.Message = taskData.Message + task.ClearText = taskData.ClearText + task.Completed = taskData.Completed + if task.Completed { agent.ClosedTasks.Put(task.TaskId, task) } else { diff --git a/AdaptixServer/core/server/utils.go b/AdaptixServer/core/server/utils.go index c010a1c0..a631e158 100644 --- a/AdaptixServer/core/server/utils.go +++ b/AdaptixServer/core/server/utils.go @@ -125,24 +125,24 @@ type DownloadData struct { } type ListingFileData struct { - IsDir bool `json:"l_is_dir"` - Size int64 `json:"l_size"` - Date int64 `json:"l_date"` - Filename string `json:"l_filename"` + IsDir bool `json:"b_is_dir"` + Size int64 `json:"b_size"` + Date int64 `json:"b_date"` + Filename string `json:"b_filename"` } type ListingProcessData struct { - Pid uint `json:"l_pid"` - Ppid uint `json:"l_ppid"` - SessionId uint `json:"l_session_id"` - Arch string `json:"l_arch"` - Context string `json:"l_context"` - ProcessName string `json:"l_process_name"` + Pid uint `json:"b_pid"` + Ppid uint `json:"b_ppid"` + SessionId uint `json:"b_session_id"` + Arch string `json:"b_arch"` + Context string `json:"b_context"` + ProcessName string `json:"b_process_name"` } type ListingDrivesData struct { - Name string `json:"l_name"` - Type string `json:"l_type"` + Name string `json:"b_name"` + Type string `json:"b_type"` } // SyncPacket @@ -341,3 +341,16 @@ type SyncPackerDownloadDelete struct { FileId string `json:"d_file_id"` } + +/// BROWSER + +type SyncPacketBrowserDisks struct { + store string + SpType int `json:"type"` + + AgentId string `json:"b_agent_id"` + Time int64 `json:"b_time"` + MessageType int `json:"b_msg_type"` + Message string `json:"b_message"` + Data string `json:"b_data"` +} diff --git a/AdaptixServer/extenders/agent_beacon/pl_agent.go b/AdaptixServer/extenders/agent_beacon/pl_agent.go index 1ff47737..8eb62d42 100644 --- a/AdaptixServer/extenders/agent_beacon/pl_agent.go +++ b/AdaptixServer/extenders/agent_beacon/pl_agent.go @@ -414,6 +414,7 @@ func ProcessTasksResult(ts Teamserver, agentData AgentData, taskData TaskData, p case COMMAND_DISKS: result := packer.ParseInt8() + var drives []ListingDrivesData if result == 0 { errorCode := packer.ParseInt32() @@ -421,10 +422,7 @@ func ProcessTasksResult(ts Teamserver, agentData AgentData, taskData TaskData, p task.MessageType = MESSAGE_ERROR } else { - drivesCount := int(packer.ParseInt32()) - var drives []ListingDrivesData - for i := 0; i < drivesCount; i++ { var driveData ListingDrivesData driveCode := packer.ParseInt8() @@ -454,6 +452,9 @@ func ProcessTasksResult(ts Teamserver, agentData AgentData, taskData TaskData, p task.Message = "List of mounted drives:" task.ClearText = OutputText } + + SyncBrowserDisks(ts, task, drives) + break case COMMAND_DOWNLOAD: @@ -693,6 +694,8 @@ func ProcessTasksResult(ts Teamserver, agentData AgentData, taskData TaskData, p } } +/// BROWSERS + func BrowserDownloadChangeState(fid string, newState int) ([]byte, error) { fileId, err := strconv.ParseInt(fid, 16, 64) if err != nil { @@ -703,3 +706,8 @@ func BrowserDownloadChangeState(fid string, newState int) ([]byte, error) { return PackArray(array) } + +func BrowserDisks() ([]byte, error) { + array := []interface{}{COMMAND_DISKS} + return PackArray(array) +} diff --git a/AdaptixServer/extenders/agent_beacon/pl_main.go b/AdaptixServer/extenders/agent_beacon/pl_main.go index 37ed12dc..8cccc78b 100644 --- a/AdaptixServer/extenders/agent_beacon/pl_main.go +++ b/AdaptixServer/extenders/agent_beacon/pl_main.go @@ -45,6 +45,8 @@ type Teamserver interface { TsDownloadAdd(agentId string, fileId string, fileName string, fileSize int) error TsDownloadUpdate(fileId string, state int, data []byte) error TsDownloadClose(fileId string, reason int) error + + TsClientBrowserDisks(jsonTask string, jsonDrives string) } type ModuleExtender struct { @@ -108,24 +110,24 @@ type TaskData struct { } type ListingFileData struct { - IsDir bool `json:"l_is_dir"` - Size uint64 `json:"l_size"` - Date uint64 `json:"l_date"` - Filename string `json:"l_filename"` + IsDir bool `json:"b_is_dir"` + Size uint64 `json:"b_size"` + Date uint64 `json:"b_date"` + Filename string `json:"b_filename"` } type ListingProcessData struct { - Pid uint `json:"l_pid"` - Ppid uint `json:"l_ppid"` - SessionId uint `json:"l_session_id"` - Arch string `json:"l_arch"` - Context string `json:"l_context"` - ProcessName string `json:"l_process_name"` + Pid uint `json:"b_pid"` + Ppid uint `json:"b_ppid"` + SessionId uint `json:"b_session_id"` + Arch string `json:"b_arch"` + Context string `json:"b_context"` + ProcessName string `json:"b_process_name"` } type ListingDrivesData struct { - Name string `json:"l_name"` - Type string `json:"l_type"` + Name string `json:"b_name"` + Type string `json:"b_type"` } var ModuleObject ModuleExtender @@ -312,7 +314,7 @@ func (m *ModuleExtender) AgentDownloadChangeState(agentObject []byte, newState i } taskData = TaskData{ - Type: JOB, + Type: BROWSER, Data: packData, Sync: false, } @@ -324,3 +326,60 @@ func (m *ModuleExtender) AgentDownloadChangeState(agentObject []byte, newState i return buffer.Bytes(), nil } + +func (m *ModuleExtender) AgentBrowserDisks(agentObject []byte) ([]byte, error) { + var ( + packData []byte + agentData AgentData + taskData TaskData + buffer bytes.Buffer + err error + ) + err = json.Unmarshal(agentObject, &agentData) + if err != nil { + return nil, err + } + + packData, err = BrowserDisks() + if err != nil { + return nil, err + } + + taskData = TaskData{ + Type: BROWSER, + Data: packData, + Sync: false, + } + + err = json.NewEncoder(&buffer).Encode(taskData) + if err != nil { + return nil, err + } + + return buffer.Bytes(), nil +} + +/// SYNC + +func SyncBrowserDisks(ts Teamserver, task TaskData, drivesSlice []ListingDrivesData) { + var ( + jsonDrives string + jsonTask string + jsonData []byte + err error + ) + + jsonData, err = json.Marshal(drivesSlice) + if err != nil { + return + } + jsonDrives = string(jsonData) + + jsonData, err = json.Marshal(task) + if err != nil { + return + } + jsonTask = string(jsonData) + + ts.TsClientBrowserDisks(jsonTask, jsonDrives) +} diff --git a/Client/CMakeLists.txt b/Client/CMakeLists.txt index e36d954f..d8d59313 100644 --- a/Client/CMakeLists.txt +++ b/Client/CMakeLists.txt @@ -47,6 +47,7 @@ SET( HEADERS Headers/Client/LastTickWorker.h Headers/Agent/Commander.h Headers/UI/Widgets/DownloadsWidget.h + Headers/UI/Widgets/BrowserFilesWidget.h ) add_executable(AdaptixClient @@ -81,6 +82,7 @@ add_executable(AdaptixClient Source/Client/LastTickWorker.cpp Source/Agent/Commander.cpp Source/UI/Widgets/DownloadWidget.cpp + Source/UI/Widgets/BrowserFilesWidget.cpp ) target_link_libraries(AdaptixClient diff --git a/Client/Headers/Agent/Agent.h b/Client/Headers/Agent/Agent.h index 02977053..d5983c9a 100644 --- a/Client/Headers/Agent/Agent.h +++ b/Client/Headers/Agent/Agent.h @@ -5,15 +5,17 @@ #include #include #include +#include #include class ConsoleWidget; +class BrowserFilesWidget; class AdaptixWidget; class Agent { public: - AdaptixWidget* mainWidget = nullptr; + AdaptixWidget* adaptixWidget = nullptr; AgentData data = {0}; @@ -33,12 +35,15 @@ public: TableWidgetItemAgent* item_Last; TableWidgetItemAgent* item_Sleep; - ConsoleWidget* Console = nullptr; + ConsoleWidget* Console = nullptr; + BrowserFilesWidget* FileBrowser = nullptr; explicit Agent(QJsonObject jsonObjAgentData, Commander* commander, AdaptixWidget* w ); ~Agent(); void Update(QJsonObject jsonObjAgentData); + + QString BrowserDisks(); }; #endif //ADAPTIXCLIENT_AGENT_H diff --git a/Client/Headers/Client/Requestor.h b/Client/Headers/Client/Requestor.h index 42137739..54db6d43 100644 --- a/Client/Headers/Client/Requestor.h +++ b/Client/Headers/Client/Requestor.h @@ -30,5 +30,7 @@ bool HttpReqAgentSetTag( QString agentId, QString tag, AuthProfile profile, QStr bool HttpReqBrowserDownload( QString action, QString fileId, AuthProfile profile, QString* message, bool* ok ); +bool HttpReqBrowserDisks( QString agentId, AuthProfile profile, QString* message, bool* ok ); + #endif //ADAPTIXCLIENT_REQUESTOR_H diff --git a/Client/Headers/UI/Widgets/AdaptixWidget.h b/Client/Headers/UI/Widgets/AdaptixWidget.h index 7177ada1..5a9a3c9b 100644 --- a/Client/Headers/UI/Widgets/AdaptixWidget.h +++ b/Client/Headers/UI/Widgets/AdaptixWidget.h @@ -83,6 +83,7 @@ public slots: void LoadDownloadsUI(); void OnReconnect(); void LoadConsoleUI(QString AgentId); + void LoadFileBrowserUI(QString AgentId); }; #endif //ADAPTIXCLIENT_ADAPTIXWIDGET_H \ No newline at end of file diff --git a/Client/Headers/UI/Widgets/BrowserFilesWidget.h b/Client/Headers/UI/Widgets/BrowserFilesWidget.h new file mode 100644 index 00000000..3eff04bc --- /dev/null +++ b/Client/Headers/UI/Widgets/BrowserFilesWidget.h @@ -0,0 +1,48 @@ +#ifndef ADAPTIXCLIENT_BROWSERFILESWIDGET_H +#define ADAPTIXCLIENT_BROWSERFILESWIDGET_H + +#include +#include + +typedef struct BrowserFileData +{ + QString Path; + QString Type; + QString Modified; + QVector Files; +} BrowserFileData; + +class BrowserFilesWidget : public QWidget +{ + QGridLayout* mainGridLayout = nullptr; + QGridLayout* listGridLayout = nullptr; + QTreeWidget* treeBrowserWidget = nullptr; + QWidget* listBrowserWidget = nullptr; + QTableWidget* tableWidget = nullptr; + QLabel* statusLabel = nullptr; + QSplitter* splitter = nullptr; + QLineEdit* pathInput = nullptr; + QPushButton* buttonHomeDir = nullptr; + QPushButton* buttonReload = nullptr; + QPushButton* buttonUpload = nullptr; + QPushButton* buttonDisks = nullptr; + QPushButton* buttonCd = nullptr; + QFrame* line_1 = nullptr; + QFrame* line_2 = nullptr; + + Agent* agent; + + void TreeAddData(BrowserFileData data); + void createUI(); + +public: + BrowserFilesWidget(Agent* a); + ~BrowserFilesWidget(); + + void SetDisks(qint64 time, int msgType, QString message, QString data); + +public slots: + void onDisks(); +}; + +#endif //ADAPTIXCLIENT_BROWSERFILESWIDGET_H \ No newline at end of file diff --git a/Client/Headers/UI/Widgets/ConsoleWidget.h b/Client/Headers/UI/Widgets/ConsoleWidget.h index b6c93e8c..8002a6a2 100644 --- a/Client/Headers/UI/Widgets/ConsoleWidget.h +++ b/Client/Headers/UI/Widgets/ConsoleWidget.h @@ -43,4 +43,4 @@ public slots: void processInput(); }; -#endif //ADAPTIXCLIENT_CONSOLEWIDGET_H +#endif //ADAPTIXCLIENT_CONSOLEWIDGET_H \ No newline at end of file diff --git a/Client/Headers/UI/Widgets/SessionsTableWidget.h b/Client/Headers/UI/Widgets/SessionsTableWidget.h index 6aa928af..36f89083 100644 --- a/Client/Headers/UI/Widgets/SessionsTableWidget.h +++ b/Client/Headers/UI/Widgets/SessionsTableWidget.h @@ -59,6 +59,7 @@ public slots: void handleSessionsTableMenu(const QPoint &pos ); void actionConsoleOpen(); + void actionFileBrowserOpen(); void actionAgentTag(); void actionAgentHide(); void actionAgentRemove(); diff --git a/Client/Headers/Utils/FileSystem.h b/Client/Headers/Utils/FileSystem.h index 6b059143..6b7e21f8 100644 --- a/Client/Headers/Utils/FileSystem.h +++ b/Client/Headers/Utils/FileSystem.h @@ -8,4 +8,6 @@ QString ReadFileString(const QString &filePath, bool* result); +QString GetRootPathWindows(const QString& path); + #endif //ADAPTIXCLIENT_FILESYSTEM_H diff --git a/Client/Headers/main.h b/Client/Headers/main.h index ca7433f0..1aa5643d 100644 --- a/Client/Headers/main.h +++ b/Client/Headers/main.h @@ -42,6 +42,7 @@ #include #include #include +#include #include #include @@ -75,6 +76,8 @@ #define TYPE_DOWNLOAD_UPDATE 0x52 #define TYPE_DOWNLOAD_DELETE 0x53 +#define TYPE_BROWSER_DISK 0x61 + ////////// #define DOWNLOAD_STATE_RUNNING 0x1 diff --git a/Client/Resources/Resources.qrc b/Client/Resources/Resources.qrc index 0f56ebc7..ba49b5b7 100755 --- a/Client/Resources/Resources.qrc +++ b/Client/Resources/Resources.qrc @@ -32,11 +32,12 @@ icons/close_dp64.png icons/double_arrow_down_64dp.png icons/autorenew_64dp.png - icons/reply_64dp.png + icons/folder_64dp.png icons/upload_file_64dp.png icons/arrow_right_alt_64dp.png icons/storage_64dp.png + icons/filesystem/ssd_64dp.png diff --git a/Client/Resources/icons/arrow_right_alt_64dp.png b/Client/Resources/icons/arrow_right_alt_64dp.png new file mode 100644 index 0000000000000000000000000000000000000000..4995360b1bebb29ce5b23acdc5b8a7dbf841e28b GIT binary patch literal 3306 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 KwuLs#=>GzKs&i-n literal 0 HcmV?d00001 diff --git a/Client/Resources/icons/autorenew_64dp.png b/Client/Resources/icons/autorenew_64dp.png new file mode 100644 index 0000000000000000000000000000000000000000..3f631835a948ea0066278f7d091e81f958a68560 GIT binary patch literal 9254 zcmZu%bzGBQ)PFVy49S6XhoFd*w8VhXE!`j~pbQvYqohGV6cD7Ok(5Rl9ZCpNii8ML zVszKL-}}e=*ZbFVKleQ6+&p!^Cr(dCmFza-Z2$ns)YTAA004w;ZqFT+ShG^H&9FeHYCL%HA(N;_=&zbF6(OoJ5J zd!Upc#N3&KGzT04_d!#}3V{&-%vUZ;2T#yrVTW)g0x)<~u5Jl6o?uAye}e5f|;An8V1CKke}|0ovVTwb>< z2qeT#vK;HW4_k7Ezgeyq7KWjdtlbK}wkn=!jkW$fZGNG!wfzAB)~KE5L_lm4O7D!A z-7^_~?NgfPDW{;2qXs7teH;}BZ0#C+DGuIzMPa}9CR&vmqlP9YVNej!(})GYYvkV1 z!&ps{OiJ8DSc&jQQ5GRWN4+~R5Tu`)^>w5g=9ZpPNg9;hfjp#C3IXQut1SsY0NKC~ zb{%fp$hSWXR5;Pc%ebIw1h`Y_h4M=TVB9jS#SsVjx=hdW5CPcjJ5e`L5{rVKM5W z?IQ#Md^@`0c|%Q%XyC*Pn%XHc*XE}DJc7RyvUwgePdUR~jG&xom$UZ!ZgG(8i2HZq zVgbb!l>-$@;>QD5y+T16gnQ*$~MUBRo=SgvemFRW8>jw4$jF|Om6O%LIBUh zKeuWfoNbJeryn2^cTzYmK(BrX#;YQB~=oZBTz#dr>!97+YX*B7Ot zvDcj7UFP3p45Z}b$$3I{``D~yXR~H)EVW9JLxXwkIcvG0YrsW3B`zlpkU92fGR#gQ zkZo?3zZ2uKxZ~+-_vD6IhyTc#D)%IMxGaBO5wcvM>eat5>~c!Sk&qZOAEkff8nYJ} zXkexL@>>i0JOZ*DE>RoSh4u{KU){22Eu(1HIJ~5i4T}g2Ws}i{*i!y+ZxW3eJgoz0 zlJazg%A~0(gvcpwt#vJpj`{ByNkzMWo=L5fE5@Q~k+UMw_E zRu_^_>|ba4RZ=FD=6IiC#*oI*Gy9-<_yPeiVz9T@sHL#H3jN89uAC2l{5=b(l2702 zke`Hq8usS|2$p|c9G^WslNsx8rS#Yk_^c@2#wm8sG`TT$&2mHpcU9AKd7km|$=H`u z@5T!gj(Ls2)E7TSL(>jIJl9SwEHq|+@04RbU_w*Faez4j)%!~R*q&#;gI4*o22OnO zE`6Mgncs%s06n9OpS5^^KaWKf?m=cSa3%_lr#~;-V*)=>C4<39{u0;^3ECj&It?{C zUu!!5`|`bk;pLj(dD++7X29&C=smHWO@m8uJZ=Q24#;;SS;dC zkQ_Mc_VbLO^YinWfSe_#>7bcY7SU@`v1{LR->Qx{U@=_F{#{Bx>!AGj;iMf9diQv3 z9Nyb9{UtbiTD$k!DfPCAG$%Ug@}I%#V5e+X;BzARr7zp<#ufL)u8k*do0xN=VPWeB zYptgLx+uM`Kg)R6y_Q(T8?Tn_<4os(Pi1oncJfF|a9VFp*CzHMZA{t)-b(?1>UbB~ z%d3>-nxLSl8`t}uqhc8?cX*GR{I_O`L^?Dd9$kiVuq*^~>H^VC|TI`x0do}DZD~K9VslVo=Gy@}-epG((`FjTI>)pk^_lX&K5cnjH ziL7$Q7^}P*^I4N#=k!Q#b@n)A={Bb(aQ0-4ihn5gH)whMQ4CwwFiLhO{KdlzQS5Al z;Om}LZeU?;P&PC){8`Fxy>TO*B%4p$O}`#!ZO%+%mB&-2ubqWP8S>xT%s$>uPhkfA zIq2ugjI|;hn&Pp;T85q+`EM2GZ*6~l4%q&Ld{98Q$yepfPmcSB@2Cw1XS4-dJYb&tos{ZYy-0;#Qfk|onRCeSXxxDA*F&*(5P$x`C|uWuy+Ca zo}M#JvHyUxX1J|Hk|p1+OPv>DXyQT)lwOT>Ka)SH>bmBg=j7$l#c6Y27uHnCGVV`r zjCJtPiPJ2EY zA?!}55Y(|)#6Jc87s{Sjvo#9wKDj)SAEPa%wjv_9AD_~x&&0`|RIjY&9ZJ1yUcwa& z(W~MttdYT>+4du*PcVfxi|A@hxjzxH-s)0B%wjOCjW>6TBIl9V?K^y6Q!{=Xt8<;R z*sj0kce}0p#a9&3s%p#0(zKdEco&n6d6+1@FmSU@qVHe5{b~{Qd$D$ofe}ofb+HuJ z+7W)2^Ho)rPGGA?n@qI`X;>ld^{vnYdGpB$ynRkxEQgjqbx z;+hxBLN(WnqunyzM#(B*KFjh&qf&HJ&iF*>F7ItoUiGDePs>E_K+q)el83#WWzkH| zVNRB;Z`HZq&rB-y7;`PZ&qN98*=?TGKBV`0ED+5=!Mh{jKSPZfs+7A6NLZ7|kW`<# z&a3uVAZ%BsuOITLL@+kRl@>o47fTl}dsfAZR%{I{LH+SV0qZ$e)8%!&fq`u|RYfrl z+sc>_j**olyD*Ywo;An0hx?qLIy~U!x&Y6MOHdTkR5{K*L@8I~_8suY<4Y!*&|HJf zx$0&=FDX63Mm_pUVs;ZgmZ4%(NRa2Y&Gs$eL2esjC+{>jn=q}OAzabUMPh*EB;xht z8BE};YxUP4+gyD3H00H&i4H8ns*peqd-A&^FYTal|LzK$9S!><7R%t-;cB-a38RKst*Xg%{#efkh|Cw}^_f2Ms?{S8;l9v1L!tZZKHe_FVavS7vFq&aUsZ%wtp32t4OAeUKp`A9VHFM*K;-+OB_ z-?0Aulg@VZ8XuO{P3*1H9c>-|^}7pp4w@o^mu&WVnB6Q!D2k7J&jO1ETYP7t@Ds)R1HMeldeV^hi^ zNbKLPY^+}xuBi)LLhooyv#l5|=~ujwPCi&$)Y@k>3g}v^;siP_{A3iv$|4h*uaTE0 zMWK;eXY-Np0z(Fj>$QZTCh7c5j}C^RVn6iDyfV-)Qf;~KEe3BRPY@mHW)t1NtFK9U zkM_%;WMZZKdHCDNTIyXh&99A{RsiXEbr{^-7#*-bHie(v8|oE!c0qVL}p zysmhv+$QCyL=*gH*N zTqb=Qw2t?C+>9^?a_w@A$C`+hnr!%#H|U8~*C&vG8x#}Llr{Z9=kge<0JP67-{G?l zg2DU#y`vPzk&F~CU#c+^wi%f%swxCQ*=-gRh_7FZi08;A*<*qpd`Y);pcFzbZ+$6q z+fbbeLHu9@3sE*~J0=3>!F_M)>+ghH7JMIXH7zp_zMmWZ9#wB+w5TiP1_kKCj|Y_V zmt4^Ycc8XxWC2u3Q1BdK2W<&!se5&A=j&lqyEOR5ObKY!aHOqiz=}~8iveW2h1~I0 zXp0G<*Y>wW#%^fbfEU5tm@xd-_`0~X(fe8(tG>3sJ)vv>_yAm7S%OA8_D@1p=4}P! z-l_X8*6{BX<%>aTz1Av!?RoY(`ndZuKwcTX1rO5If$MaU&raFqR|L8^6y@3xm`WE?0})-=SZ5o=;Ic^-Frw zsFUM48K@)ORYKts>`?DHxw-fH@>(Cvxzxt^@Dg79`L|aQ z-BeywhmRpONoGY`&&9M8nw$+Eunqs?cqQ0XPv`B*m>b0}-FOu8`Sa(R&5S;_XO&QZ z3r)0pZ;1DM!3v!{@3Om3KpN>|@cdIBo9;g~Vq2d!{z+z960NNOEO@89?xM zVGFT8&kJy<>a;ORuleLdTZ&%BiN%-*xGb2Z9au6TjB6M~sw;(rnMm1FefsY{UWSaF znizgbAm3=0{nuyqpHiDC180*H&Y39O6cG0m?1H8v(B}uPnSDc_OdC^TOyIzVAZevj z)jek#5GgsCcCOHt%l4{)tzLgN6-<~CeZhm#GV&HCkYqMMKgKKKn}&G661kkue4VaO zIN~XnJIrnMrZk0e4g%`S{VK`7J|nm`Uh)X2PCbU9*QK71%}Y!U<2>#MWkQq^;Nmt? z%2409m*Hoq`bvs~*iBK?DW5AB$TA{TER{C8qwTj;T-57fwh6`b>7ccRe_mEo z>W@;ee8GOJyEGjP-4Z%l{wI~x`~c-;lHQmMSU&REaZ~F5m|4kEPRO7h4FfGaI%AmW zlP|XW*;{77Hv7sWp{y99_Zgfb5o*LD+$uA`8_b=RQ`Wdv^j=<_kT_hi;M{I_jq6_e zMteGCqsR5mIYtAF@f42}f6c|Z1K~CN? zx6$Zkt$loSWajOn)EbetoC#e%Tq4c6wkRQjg^|yGJ(-yL#`?^S7??uWJY_^T0!JyD z?nEWe9_z6vT-=!e@lWBKj|P}MVnzFsXoFuT-| zxz$;j%#o?W8vX2#E(-O@V8#`Urf%*AkKG0&8)+!*4s**LW>Z@ljog%zCtnhQsyJ98 z3vS4=X+K*0Q%*2aEuW&YJI+QhDAJe~6#-OCZ@gZ~?(q4ps#LaDIh{zU;YgCv3keL% z6gL}``QAX}Q12Ehh&ArGZjiE2QUI7rKt)v>PgZBku?1p zt$sgkwj5=GA?@9ztu>^5zLan4q>grGQ|{TF40{|Q_qiw~$sadLOof#HitVsE$1~dV z>D|jsPGERD$&puA`Il#6ED3O24(RYvSqMCyweASy{AY)`4+Wi>aL zsNF)R+W9Ap?kw-Wr)yZY7Nf+k!`v5(?tH5tHXD*0Wgq%5)wF+Xj0$w;;7c~2<>F!q(aU{BKUo?aE@%V zA#=wT_dm#pC)RP{AF(IkpZR+d1dU6za`@>AX(gKH=)hF>-hTN>{`k_%5T`V1`#=!W zHhJcRBG4p(!D}z=K8fNpuYF|Fk-hGx)`1%*9Bn#AWKO1hOz?XG<5d$A(tej%T!;DH zd+DRQMPtOVBAjTnWDiRCHB(nVH}Yb;dGJ_%IZyC)VxHq|UDBJbSZt?H4cR-sWKUTH zFu?J=OL2R^<3927ub~6j`u4jL8T3))yISQ9fy;2Bo--IIYNCelee0q>se8Xvc^iF`=gK&d=|wpNv9@;0K!oTfdz@;*JS-=1sq!pe_p=udMmvP2gAR0)6c;qqO=HEc;|46d=hNB@9y|U- zol6X$2rXeSIELj@>+ja2CA%Kk6~An$N2i*W(zhg!Xom*6IH*3sd$H_*H=ByCQer|) z_+hBKj@zyn9(-6*Eqb#{AbPE{nw0lBYSD+itf2aKw4(Wb%g;>-Hi92E4Eo{1fo= zF4c|D$Df2D2teHb*@wiW0l{ylt-}L?HYjc;c<=>A?AlV`t;5p=b?Bb}s=%8!)?uy# zA14A-6Z^-ti%VSOC>FI|bZaR=D7CseDh;hgLl4WoWMLL0zR|QaZ!;=|cIzr$K3`S3 zh9SekZ|PtO&d}*>M(I!t#cQ%Zuwo@Sf2 z900~ubiAexXsuiJYilhWSga*I zhZ*b8`_*WswnoBeJ(Detdy&*8PwbyUSO z^F~)AY}=8Kgyq|d13_2w?6*CK|GMe1bR}ijFgh0mFgnX+^W=rgB@1rP+CGU*=bDLj zn()W3Q%_WDGmm}DQ>FvGDjeHiqWsz`f$m%n-t^(6i`2a50eFz+tGylbu|$(lR_1sX zW?QzkJ>^vwyXh6CY+=iwI|B+Q5TkIYpWm91Y03y18nhfApSOXrjc=c6cf`%6O+Se5 zKS@sCYbEzy=lC#VnL>B6XeB;;UCK2ku!X}}e|yr+^(GU_&Q4CDG`?gyDoD2dutkQ{ zZb+WOh5C6o9Y_{gdZYRG%%u5lSc1r1uuDV3!h87{;YF+6$5V#-$iAbRzdpYRx-7K+ zF>C7zNPLRCGv+ROI^XjCF;h~gxWFbVC=2W4qj}7mE>rW|f`R`_CNzJPyrCF7 zC?mHjS;xXG;M`|_c{OS04Hvxt%h`LL9$I$4oi*LwEo0k$PNT$$HikXQT#Ot@NzPn) zQ0#3}+5d+;MH8pxy(US^C7F&h`0>-0%#M&2w@dx{rChwD=pT>iUmnH0V7-?*R>=O_ zQXF#C5d`ut+~;bn858b48>-os;RK#TElifx$CmM6mByHwN(^@qu7P3g#`N*myR}Pt6S~bx0@`O!0A6+!7#M$uEFXo;klSg zODB(;gqCLMP5#HEJED~A;N1dFcb7}*iGz1RpC)6T67i61iOWzbt?Ck-hNofB(@zy* z0dt7fU8&JCBR#jF?-R;a_Z_Z+rN0*?t~e3I0Uh0q1wKe5^71q;{ao0iSSsv+q~@{e z;kn-dd8kQA<>r|&;{J0tJQDorrHN+w!Pz93Cu#?~Yr*GGOI6b|^S9A*rc;&9|I(8b zhK`YR#;3ke-#SVY8(9+1HM_kK!%@0N>kBM&TQ~%w*ZTr`%wvXkN{JmoG{*IFx3~tK zWXDFOk96%c57&L%tlOJvYCLj%iYXMr&UT*bSo}< zMf?MF-!40^s_{tKFwwkwfGP}2Qlti^zmkb_=}OqBzjpE9QwB+eRCtd%;1L*7jhB-M z7isj0Oqj;Vc}-?7t+PlRZ(WPYBM4Wya4Y~rD+s;QpQa~9iP91(Jz~MgTnqYFBFnjc z*|!}PR~cLU`1$i9@R7e>_Aw_MGC_Fxkd^q4zTF$mF>4_Fb8LFjm(8lqVE@rK4FVv98|tsn5Mv4`3S|N^M5HF$Vz$`<;MU52wE$=B4|wm^cy=4VnCLcXPYPyc zl*=73P!O9F!NZe>x&kKiaf6xdLZ?y&{cy>>82_9j=eG7dk>d2jC^`@)TKu9(0gS_; zU$3j7>tZ19FLg@W!+ts)+`B;k2krwzt=yQWq<(~RD<$!_tSC6tcvI2$;Yt-Tcr{EO zHR5|5C%rlNzEzkC{fxps{#zcdBoz){U~YMeHwHs@Xt^+GPm5agrr)FV(^79A3Mp@&y0Hu%gaXI>5NYcz_EFewdvc&!a)bi{8o33P#J6>*7ajjmR1p*DtSH?uVUZAG3 z-S_N%ioak~#E{{6){JNtUtXE^OIJ0mYnpmIUX*Use5?e>9yU=C*2WJsnZ^J?9-Lw&jm zn4_F`M>G=3j!1rphR~}4B$0&bXhGI2a(;4{p~J|3$H`HNj{<18C+1j$l)yPl7QJG> zpOt_2=7@~{3i^*+MJPb5wtl$E(y8?6YtcXrK^|Gpe=b|SAn^w`Jjn*+bAgBjwhqju z?g|(RQ(@}tpr6F4@&}eg2by@f!L`34E$-cnDB4N*`Q<;u_p|LY)|KN69$-ibUm{o{wHUP(efVY_RC0FT+ecM5g48%pLP_Xg0J(q$ixpOuh@uI0aAUA zEx14snr@3ICKd4aQ!Xnu7YL=oiN~R6fMV@=xc{-mKdC*aBAs-49H5AuTqpHX2kti) z3@kBQ+i5F9D2>O{tfJu5cta)%qPoig>50b|wV29k`oh54?0BVysJ)J&gg(Hp-tLHh zUKuJHj;B+z>r*MRSK*nWrW+jSI3^%?x;7f24h^rxTY~>bDCXuCt%!Z2qVy6LU;hKB ME9)St6>TH_2hKMT8vp-n~=IE*WNM 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 literal 0 HcmV?d00001 diff --git a/Client/Source/Agent/Agent.cpp b/Client/Source/Agent/Agent.cpp index 2ff966b5..30921e83 100644 --- a/Client/Source/Agent/Agent.cpp +++ b/Client/Source/Agent/Agent.cpp @@ -1,8 +1,9 @@ #include +#include Agent::Agent(QJsonObject jsonObjAgentData, Commander* commander, AdaptixWidget* w) { - mainWidget = w; + adaptixWidget = w; data.Id = jsonObjAgentData["a_id"].toString(); data.Name = jsonObjAgentData["a_name"].toString(); @@ -53,9 +54,12 @@ Agent::Agent(QJsonObject jsonObjAgentData, Commander* commander, AdaptixWidget* item_Sleep = new TableWidgetItemAgent( sleep, this ); item_Pid = new TableWidgetItemAgent( data.Pid, this ); - Console = new ConsoleWidget(this, commander ); + Console = new ConsoleWidget(this, commander ); + FileBrowser = new BrowserFilesWidget(this); } +Agent::~Agent() = default; + void Agent::Update(QJsonObject jsonObjAgentData) { data.Sleep = jsonObjAgentData["a_sleep"].toDouble(); @@ -75,4 +79,14 @@ void Agent::Update(QJsonObject jsonObjAgentData) item_Sleep->setText(sleep); } -Agent::~Agent() = default; +QString Agent::BrowserDisks() +{ + QString message = QString(); + bool ok = false; + bool result = HttpReqBrowserDisks( data.Id, *(adaptixWidget->GetProfile()), &message, &ok); + if (!result) + return "JWT error"; + + return message; +} + diff --git a/Client/Source/Client/ProcessSyncPacket.cpp b/Client/Source/Client/ProcessSyncPacket.cpp index 1c39a6c5..83b02ca1 100644 --- a/Client/Source/Client/ProcessSyncPacket.cpp +++ b/Client/Source/Client/ProcessSyncPacket.cpp @@ -316,13 +316,32 @@ bool AdaptixWidget::isValidSyncPacket(QJsonObject jsonObj) } return true; } - if(spType == TYPE_DOWNLOAD_DELETE ) { + if( spType == TYPE_DOWNLOAD_DELETE ) { if (!jsonObj.contains("d_file_id") || !jsonObj["d_file_id"].isString()) { return false; } return true; } + if( spType == TYPE_BROWSER_DISK ) { + if (!jsonObj.contains("b_agent_id") || !jsonObj["b_agent_id"].isString()) { + return false; + } + if (!jsonObj.contains("b_time") || !jsonObj["b_time"].isDouble()) { + return false; + } + if (!jsonObj.contains("b_msg_type") || !jsonObj["b_msg_type"].isDouble()) { + return false; + } + if (!jsonObj.contains("b_message") || !jsonObj["b_message"].isString()) { + return false; + } + if (!jsonObj.contains("b_data") || !jsonObj["b_data"].isString()) { + return false; + } + return true; + } + return false; } @@ -574,4 +593,18 @@ void AdaptixWidget::processSyncPacket(QJsonObject jsonObj) return; } + + if( spType == TYPE_BROWSER_DISK ) + { + QString agentId = jsonObj["b_agent_id"].toString(); + qint64 time = jsonObj["b_time"].toDouble(); + int msgType = jsonObj["b_msg_type"].toDouble(); + QString message = jsonObj["b_message"].toString(); + QString data = jsonObj["b_data"].toString(); + + if (Agents.contains(agentId)) + Agents[agentId]->FileBrowser->SetDisks(time, msgType, message, data); + + return; + } } diff --git a/Client/Source/Client/Requestor.cpp b/Client/Source/Client/Requestor.cpp index ef21d591..8e837943 100644 --- a/Client/Source/Client/Requestor.cpp +++ b/Client/Source/Client/Requestor.cpp @@ -190,4 +190,20 @@ bool HttpReqBrowserDownload( QString action, QString fileId, AuthProfile profile return true; } return false; +} + +bool HttpReqBrowserDisks( QString agentId, AuthProfile profile, QString* message, bool* ok ) +{ + QJsonObject dataJson; + dataJson["agent_id"] = agentId; + QByteArray jsonData = QJsonDocument(dataJson).toJson(); + + QString sUrl = profile.GetURL() + "/browser/disks"; + QJsonObject jsonObject = HttpReq(sUrl, jsonData, profile.GetAccessToken()); + if ( jsonObject.contains("message") && jsonObject.contains("ok") ) { + *message = jsonObject["message"].toString(); + *ok = jsonObject["ok"].toBool(); + return true; + } + return false; } \ No newline at end of file diff --git a/Client/Source/UI/Widgets/AdaptixWidget.cpp b/Client/Source/UI/Widgets/AdaptixWidget.cpp index 01603d05..edd67907 100644 --- a/Client/Source/UI/Widgets/AdaptixWidget.cpp +++ b/Client/Source/UI/Widgets/AdaptixWidget.cpp @@ -294,6 +294,15 @@ void AdaptixWidget::LoadConsoleUI(QString AgentId) this->AddTab(Agents[AgentId]->Console, text); } +void AdaptixWidget::LoadFileBrowserUI(QString AgentId) +{ + if( !Agents.contains(AgentId) ) + return; + + auto text = QString("Files [%1]").arg( AgentId ); + this->AddTab(Agents[AgentId]->FileBrowser, text); +} + void AdaptixWidget::ChannelClose() { QIcon onReconnectButton = RecolorIcon(QIcon(":/icons/unlink"), COLOR_ChiliPepper); diff --git a/Client/Source/UI/Widgets/BrowserFilesWidget.cpp b/Client/Source/UI/Widgets/BrowserFilesWidget.cpp new file mode 100644 index 00000000..bfef3ba2 --- /dev/null +++ b/Client/Source/UI/Widgets/BrowserFilesWidget.cpp @@ -0,0 +1,163 @@ +#include +#include + +BrowserFilesWidget::BrowserFilesWidget(Agent* a) +{ + agent = a; + this->createUI(); + + connect( buttonDisks, &QPushButton::clicked, this, &BrowserFilesWidget::onDisks); +} + +BrowserFilesWidget::~BrowserFilesWidget() = default; + +void BrowserFilesWidget::createUI() +{ + buttonReload = new QPushButton(QIcon(":/icons/reload"), "", this); + buttonReload->setIconSize( QSize( 24,24 )); + buttonReload->setFixedSize(37, 28); + buttonReload->setToolTip("Reload"); + + buttonHomeDir = new QPushButton( QIcon(":/icons/folder"), "", this); + buttonHomeDir->setIconSize( QSize( 24,24 )); + buttonHomeDir->setFixedSize(37, 28); + buttonHomeDir->setToolTip("Up folder"); + + pathInput = new QLineEdit(this); + + buttonCd = new QPushButton(QIcon(":/icons/arrow_right"), "", this); + buttonCd->setIconSize( QSize( 24,24 )); + buttonCd->setFixedSize(37, 28); + + line_1 = new QFrame(this); + line_1->setFrameShape(QFrame::VLine); + line_1->setMinimumHeight(25); + + buttonDisks = new QPushButton(QIcon(":/icons/storage"), "", this); + buttonDisks->setIconSize( QSize( 24,24 )); + buttonDisks->setFixedSize(37, 28); + buttonDisks->setToolTip("Disks list"); + + buttonUpload = new QPushButton(QIcon(":/icons/upload"), "", this); + buttonUpload->setIconSize( QSize( 24,24 )); + buttonUpload->setFixedSize(37, 28); + buttonUpload->setToolTip("Upload File"); + + line_2 = new QFrame(this); + line_2->setFrameShape(QFrame::VLine); + line_2->setMinimumHeight(25); + + statusLabel = new QLabel(this); + statusLabel->setText("Status: "); + + tableWidget = new QTableWidget(this ); + tableWidget->setColumnCount(3); + tableWidget->setContextMenuPolicy( Qt::CustomContextMenu ); + tableWidget->setAutoFillBackground( false ); + tableWidget->setShowGrid( false ); + tableWidget->setSortingEnabled( false ); + tableWidget->setWordWrap( true ); + tableWidget->setCornerButtonEnabled( true ); + tableWidget->setSelectionBehavior( QAbstractItemView::SelectRows ); + tableWidget->setFocusPolicy( Qt::NoFocus ); + tableWidget->setAlternatingRowColors( true ); + tableWidget->horizontalHeader()->setSectionResizeMode( QHeaderView::Stretch ); + tableWidget->horizontalHeader()->setCascadingSectionResizes( true ); + tableWidget->horizontalHeader()->setHighlightSections( false ); + tableWidget->verticalHeader()->setVisible( false ); + + tableWidget->setHorizontalHeaderItem( 0, new QTableWidgetItem( "Name" ) ); + tableWidget->setHorizontalHeaderItem( 1, new QTableWidgetItem( "Size" ) ); + tableWidget->setHorizontalHeaderItem( 2, new QTableWidgetItem( "Last Modified" ) ); + + listGridLayout = new QGridLayout(this); + listGridLayout->setContentsMargins(5, 4, 1, 1); + listGridLayout->setVerticalSpacing(4); + listGridLayout->setHorizontalSpacing(8); + + listGridLayout->addWidget( buttonReload, 0, 0, 1, 1 ); + listGridLayout->addWidget( buttonHomeDir, 0, 1, 1, 1 ); + listGridLayout->addWidget( pathInput, 0, 2, 1, 5 ); + listGridLayout->addWidget( buttonCd, 0, 7, 1, 1 ); + listGridLayout->addWidget( line_1, 0, 8, 1, 1 ); + listGridLayout->addWidget( buttonUpload, 0, 9, 1, 1 ); + listGridLayout->addWidget( buttonDisks, 0, 10, 1, 1 ); + listGridLayout->addWidget( line_2, 0, 11, 1, 1 ); + listGridLayout->addWidget( statusLabel, 0, 12, 1, 1 ); + listGridLayout->addWidget( tableWidget, 1, 0, 1, 13 ); + + listBrowserWidget = new QWidget(this); + listBrowserWidget->setLayout(listGridLayout); + + treeBrowserWidget = new QTreeWidget(); + treeBrowserWidget->setSortingEnabled(false); + treeBrowserWidget->headerItem()->setText( 0, "Directory Tree" ); + + splitter = new QSplitter( this ); + splitter->setOrientation( Qt::Horizontal ); + splitter->addWidget( treeBrowserWidget ); + splitter->addWidget( listBrowserWidget ); + splitter->setSizes( QList() << 1 << 250 ); + + mainGridLayout = new QGridLayout(this ); + mainGridLayout->setContentsMargins(0, 0, 0, 0 ); + mainGridLayout->addWidget(splitter, 0, 0, 1, 1 ); + + this->setLayout(mainGridLayout); +} + +void BrowserFilesWidget::SetDisks(qint64 time, int msgType, QString message, QString data) +{ + QString sTime = UnixTimestampGlobalToStringLocal(time); + QString status = ""; + if( msgType == CONSOLE_OUT_LOCAL_ERROR || msgType == CONSOLE_OUT_ERROR ) { + status = TextColorHtml(message, COLOR_Berry) + " >> " + sTime; + } + else { + status = TextColorHtml(message, COLOR_NeonGreen) + " >> " + sTime; + } + + statusLabel->setText(status); + + QJsonDocument jsonDoc = QJsonDocument::fromJson(data.toUtf8()); + if (!jsonDoc.isArray()) + return; + + QJsonArray jsonArray = jsonDoc.array(); + for (const QJsonValue& value : jsonArray) { + QJsonObject jsonObject = value.toObject(); + BrowserFileData fileData; + fileData.Path = jsonObject["b_name"].toString(); + fileData.Type = "Disk"; + fileData.Modified = jsonObject["b_type"].toString(); + this->TreeAddData(fileData); + } +} + +void BrowserFilesWidget::TreeAddData(BrowserFileData data) +{ + QString rootPath = GetRootPathWindows(data.Path); + QTreeWidgetItem* rootItem = nullptr; + for (int i = 0; i < treeBrowserWidget->topLevelItemCount(); ++i) { + auto item = treeBrowserWidget->topLevelItem(i); + if (item->text(0) == rootPath) { + rootItem = item; + break; + } + } + + if (!rootItem) { + rootItem = new QTreeWidgetItem(treeBrowserWidget); + rootItem->setText(0, rootPath); + rootItem->setIcon(0, QIcon(":/icons/ssd")); + treeBrowserWidget->addTopLevelItem(rootItem); + } +} + +/// SLOTS + +void BrowserFilesWidget::onDisks() +{ + QString status = agent->BrowserDisks(); + statusLabel->setText(status); +} \ No newline at end of file diff --git a/Client/Source/UI/Widgets/ConsoleWidget.cpp b/Client/Source/UI/Widgets/ConsoleWidget.cpp index 2f20e7cc..6d8227c5 100644 --- a/Client/Source/UI/Widgets/ConsoleWidget.cpp +++ b/Client/Source/UI/Widgets/ConsoleWidget.cpp @@ -156,7 +156,7 @@ void ConsoleWidget::processInput() else { QString message = QString(); bool ok = false; - bool result = HttpReqAgentCommand(agent->data.Name, agent->data.Id, commandLine, cmdResult.message, *(agent->mainWidget->GetProfile()), &message, &ok); + bool result = HttpReqAgentCommand(agent->data.Name, agent->data.Id, commandLine, cmdResult.message, *(agent->adaptixWidget->GetProfile()), &message, &ok); if( !result ) { MessageError("JWT error"); return; diff --git a/Client/Source/UI/Widgets/SessionsTableWidget.cpp b/Client/Source/UI/Widgets/SessionsTableWidget.cpp index cef9519d..0ab47ef4 100644 --- a/Client/Source/UI/Widgets/SessionsTableWidget.cpp +++ b/Client/Source/UI/Widgets/SessionsTableWidget.cpp @@ -76,6 +76,7 @@ void SessionsTableWidget::Clear() Agent* agent = adaptixWidget->Agents[agentId]; adaptixWidget->Agents.remove(agentId); delete agent->Console; + delete agent->FileBrowser; delete agent; } @@ -131,6 +132,7 @@ void SessionsTableWidget::RemoveAgentItem(QString agentId) Agent* agent = adaptixWidget->Agents[agentId]; adaptixWidget->Agents.remove(agentId); delete agent->Console; + delete agent->FileBrowser; delete agent; for( int rowIndex = 0 ; rowIndex < tableWidget->rowCount() ; rowIndex++ ) { @@ -142,6 +144,7 @@ void SessionsTableWidget::RemoveAgentItem(QString agentId) } + /// SLOTS void SessionsTableWidget::handleTableDoubleClicked(const QModelIndex &index) @@ -157,12 +160,24 @@ void SessionsTableWidget::handleSessionsTableMenu(const QPoint &pos ) if ( ! tableWidget->itemAt(pos) ) return; + auto ctxMenu = QMenu(); + + auto agentMenu = new QMenu("Agent", &ctxMenu); + agentMenu->addAction("Command"); + agentMenu->addAction("File Browser", this, &SessionsTableWidget::actionFileBrowserOpen); + agentMenu->addAction("Process Browser"); + agentMenu->addAction("Exit"); + auto sep1 = new QAction(); sep1->setSeparator( true ); - auto ctxMenu = QMenu(); + auto sep2 = new QAction(); + sep2->setSeparator( true ); + ctxMenu.addAction( "Console", this, &SessionsTableWidget::actionConsoleOpen); ctxMenu.addAction( sep1 ); + ctxMenu.addMenu(agentMenu); + ctxMenu.addAction( sep2 ); ctxMenu.addAction( "Tag", this, &SessionsTableWidget::actionAgentTag); ctxMenu.addAction( "Hide", this, &SessionsTableWidget::actionAgentHide); ctxMenu.addAction( "Remove", this, &SessionsTableWidget::actionAgentRemove); @@ -181,6 +196,17 @@ void SessionsTableWidget::actionConsoleOpen() } } +void SessionsTableWidget::actionFileBrowserOpen() +{ + auto adaptixWidget = qobject_cast( mainWidget ); + for( int rowIndex = 0 ; rowIndex < tableWidget->rowCount() ; rowIndex++ ) { + if ( tableWidget->item(rowIndex, 0)->isSelected() ) { + auto agentId = tableWidget->item( rowIndex, ColumnAgentID )->text(); + adaptixWidget->LoadFileBrowserUI(agentId); + } + } +} + void SessionsTableWidget::actionAgentTag() { auto adaptixWidget = qobject_cast( mainWidget ); @@ -237,4 +263,4 @@ void SessionsTableWidget::actionAgentRemove() return; } } -} \ No newline at end of file +} diff --git a/Client/Source/Utils/FileSystem.cpp b/Client/Source/Utils/FileSystem.cpp index 8ca6b691..442f7a91 100644 --- a/Client/Source/Utils/FileSystem.cpp +++ b/Client/Source/Utils/FileSystem.cpp @@ -14,4 +14,21 @@ QString ReadFileString(const QString &filePath, bool* result) *result = true; return content; +} + +QString GetRootPathWindows(const QString& path) +{ + if (path.startsWith("\\\\")) { + int secondSlash = path.indexOf('\\', 2); + if (secondSlash != -1) { + return path.left(secondSlash); + } + } + + int firstSlash = path.indexOf('\\'); + if (firstSlash != -1) { + return path.left(firstSlash); + } + + return path; } \ No newline at end of file