diff --git a/AdaptixServer/extenders/hosting_service/Makefile b/AdaptixServer/extenders/hosting_service/Makefile new file mode 100644 index 00000000..13d860e2 --- /dev/null +++ b/AdaptixServer/extenders/hosting_service/Makefile @@ -0,0 +1,9 @@ +all: clean + @ echo " * Building hosting service plugin" + @ mkdir dist + @ cp config.yaml ax_config.axs ./dist/ + @ GOEXPERIMENT=jsonv2,greenteagc go build -buildmode=plugin -ldflags="-s -w" -o ./dist/service_hosting.so pl_main.go + @ echo " done..." + +clean: + @ rm -rf dist diff --git a/AdaptixServer/extenders/hosting_service/ax_config.axs b/AdaptixServer/extenders/hosting_service/ax_config.axs new file mode 100644 index 00000000..0fbb71fa --- /dev/null +++ b/AdaptixServer/extenders/hosting_service/ax_config.axs @@ -0,0 +1,320 @@ +/// Hosting Service - UI + +let hostingDock = null; +let hostingTable = null; +let filesData = []; + +// ============================================================================ +// InitService - Called when service is loaded +// ============================================================================ + +function InitService() { + createHostingDock(); + loadFiles(); +} + +// ============================================================================ +// Data Handler - Receives data from server +// ============================================================================ + +function data_handler(data) { + try { + let json = JSON.parse(data); + let msgType = json.type; + + if (msgType === "files") { + filesData = json.data || []; + refreshTable(); + } + else if (msgType === "url") { + ax.clipboard_set(json.data.url); + ax.show_message("Hosting", "URL copied to clipboard:\n" + json.data.url); + } + else if (msgType === "event") { + handleEvent(json.event, json.data); + } + else if (msgType === "error") { + ax.show_message("Hosting Error", json.message); + } + } catch (e) { + ax.log_error("Hosting: parse error: " + e); + } +} + +// ============================================================================ +// Hosting Dock +// ============================================================================ + +function createHostingDock() { + hostingDock = form.create_ext_dock("hosting_files", "Hosted Files", ""); + + let mainLayout = form.create_vlayout(); + + // Toolbar + let toolbar = form.create_hlayout(); + + let btnAdd = form.create_button("Add File"); + let btnDelete = form.create_button("Delete"); + let btnToggle = form.create_button("Toggle"); + let btnCopyURL = form.create_button("Copy URL"); + let btnRefresh = form.create_button("Refresh"); + + toolbar.addWidget(btnAdd); + toolbar.addWidget(btnToggle); + toolbar.addWidget(btnCopyURL); + toolbar.addWidget(btnDelete); + toolbar.addWidget(form.create_hspacer()); + toolbar.addWidget(btnRefresh); + + let toolbarPanel = form.create_panel(); + toolbarPanel.setLayout(toolbar); + mainLayout.addWidget(toolbarPanel); + + // Table + hostingTable = form.create_table(["Path", "Filename", "MIME", "Size", "Downloads", "Status", "Created By", "Created"]); + hostingTable.setSortingEnabled(true); + hostingTable.setReadOnly(true); + mainLayout.addWidget(hostingTable); + + hostingDock.setLayout(mainLayout); + hostingDock.setSize(1000, 400); + hostingDock.show(); + + // Signals + form.connect(btnAdd, "clicked", function() { + showAddFileDialog(); + }); + + form.connect(btnDelete, "clicked", function() { + let rows = hostingTable.selectedRows(); + if (rows.length === 0) return; + let fid = getFileIDByRow(rows[0]); + if (fid) { + if (ax.prompt_confirm("Delete File", "Remove this hosted file?")) { + ax.service_command("Hosting", "delete", {id: fid}); + } + } + }); + + form.connect(btnToggle, "clicked", function() { + let rows = hostingTable.selectedRows(); + if (rows.length === 0) return; + let fid = getFileIDByRow(rows[0]); + if (fid) { + ax.service_command("Hosting", "toggle", {id: fid}); + } + }); + + form.connect(btnCopyURL, "clicked", function() { + let rows = hostingTable.selectedRows(); + if (rows.length === 0) return; + let fid = getFileIDByRow(rows[0]); + if (fid) { + ax.service_command("Hosting", "copyurl", {id: fid}); + } + }); + + form.connect(btnRefresh, "clicked", function() { + loadFiles(); + }); +} + +// ============================================================================ +// Add File Dialog +// ============================================================================ + +function showAddFileDialog() { + let dialog = form.create_dialog("Host File"); + dialog.setSize(600, 500); + + let pageLayout = form.create_vlayout(); + + // --- File Selection --- + let fileGrid = form.create_gridlayout(); + + let txtFilePath = form.create_textline(""); + txtFilePath.setPlaceholder("Select a file to host..."); + txtFilePath.setReadOnly(true); + let btnBrowse = form.create_button("Browse"); + let fileRow = form.create_hlayout(); + fileRow.addWidget(txtFilePath); + fileRow.addWidget(btnBrowse); + let filePanel = form.create_panel(); + filePanel.setLayout(fileRow); + + fileGrid.addWidget(form.create_label("File *"), 0, 0); + fileGrid.addWidget(filePanel, 0, 1); + + let txtPath = form.create_textline(""); + txtPath.setPlaceholder("/downloads/payload.exe"); + fileGrid.addWidget(form.create_label("URL Path"), 1, 0); + fileGrid.addWidget(txtPath, 1, 1); + + let txtMime = form.create_textline("application/octet-stream"); + fileGrid.addWidget(form.create_label("MIME Type"), 2, 0); + fileGrid.addWidget(txtMime, 2, 1); + + let fileInner = form.create_panel(); + fileInner.setLayout(fileGrid); + let grpFile = form.create_groupbox("File", false); + grpFile.setPanel(fileInner); + pageLayout.addWidget(grpFile); + + // --- Protections --- + let protGrid = form.create_gridlayout(); + + let chkOneShot = form.create_check("One-shot (auto-disable after first download)"); + protGrid.addWidget(chkOneShot, 0, 0, 1, 2); + + let chkEncrypt = form.create_check("AES-256-CBC encrypt content (key in X-Enc-Key header)"); + protGrid.addWidget(chkEncrypt, 1, 0, 1, 2); + + let txtUA = form.create_textline(""); + txtUA.setPlaceholder("Mozilla.*Windows.* (regex, empty = allow all)"); + protGrid.addWidget(form.create_label("UA Filter"), 2, 0); + protGrid.addWidget(txtUA, 2, 1); + + let spinMaxDL = form.create_spin(); + spinMaxDL.setRange(0, 999999); + spinMaxDL.setValue(0); + let maxDLRow = form.create_hlayout(); + maxDLRow.addWidget(spinMaxDL); + maxDLRow.addWidget(form.create_label(" 0 = unlimited")); + let maxDLPanel = form.create_panel(); + maxDLPanel.setLayout(maxDLRow); + protGrid.addWidget(form.create_label("Max Downloads"), 3, 0); + protGrid.addWidget(maxDLPanel, 3, 1); + + let txtExpiry = form.create_textline(""); + txtExpiry.setPlaceholder("2025-12-31T23:59:59 (empty = no expiration)"); + protGrid.addWidget(form.create_label("Expires At"), 4, 0); + protGrid.addWidget(txtExpiry, 4, 1); + + let protInner = form.create_panel(); + protInner.setLayout(protGrid); + let grpProt = form.create_groupbox("Protections", false); + grpProt.setPanel(protInner); + pageLayout.addWidget(grpProt); + + pageLayout.addWidget(form.create_vspacer()); + + dialog.setLayout(pageLayout); + + // Browse button + let selectedFilePath = ""; + form.connect(btnBrowse, "clicked", function() { + let path = ax.prompt_open_file("Select file to host", "All Files (*)"); + if (path) { + selectedFilePath = path; + txtFilePath.setText(path); + // Auto-fill path from filename + let parts = path.replace(/\\/g, "/").split("/"); + let fname = parts[parts.length - 1]; + if (!txtPath.text()) { + txtPath.setText("/" + fname); + } + } + }); + + let accepted = dialog.exec(); + if (accepted === true) { + if (!selectedFilePath) { + ax.show_message("Error", "Please select a file to host"); + return; + } + + let content = ax.file_read_base64(selectedFilePath); + if (!content) { + ax.show_message("Error", "Failed to read file"); + return; + } + + let parts = selectedFilePath.replace(/\\/g, "/").split("/"); + let fname = parts[parts.length - 1]; + + let req = { + filename: fname, + content: content, + path: txtPath.text(), + mime_type: txtMime.text(), + one_shot: chkOneShot.isChecked(), + encrypted: chkEncrypt.isChecked(), + ua_filter: txtUA.text(), + max_downloads: spinMaxDL.value(), + expires_at: txtExpiry.text() + }; + + ax.service_command("Hosting", "add", req); + } +} + +// ============================================================================ +// Table Helpers +// ============================================================================ + +function refreshTable() { + if (!hostingTable) return; + + hostingTable.setRowCount(0); + if (!filesData) return; + + for (let i = 0; i < filesData.length; i++) { + let f = filesData[i]; + let created = f.created_at ? f.created_at : ""; + let status = f.enabled ? "Active" : "Disabled"; + let dlStr = String(f.downloads || 0); + if (f.max_downloads > 0) { + dlStr += " / " + String(f.max_downloads); + } + + let sizeStr = formatSize(f.content_size || 0); + + hostingTable.addItem([ + f.path || "", + f.filename || "", + f.mime_type || "", + sizeStr, + dlStr, + status, + f.created_by || "", + created + ]); + } +} + +function getFileIDByRow(row) { + if (!filesData || row < 0 || row >= filesData.length) return null; + return filesData[row].id; +} + +function formatSize(bytes) { + if (bytes === 0) return "0 B"; + let units = ["B", "KB", "MB", "GB"]; + let i = 0; + let size = bytes; + while (size >= 1024 && i < units.length - 1) { + size = size / 1024; + i++; + } + if (i === 0) return String(size) + " B"; + return size.toFixed(1) + " " + units[i]; +} + +// ============================================================================ +// Event Handling +// ============================================================================ + +function handleEvent(eventType, data) { + if (eventType === "download") { + // Reload file list to reflect updated download count + loadFiles(); + } +} + +// ============================================================================ +// Data Loading +// ============================================================================ + +function loadFiles() { + ax.service_command("Hosting", "list", {}); +} diff --git a/AdaptixServer/extenders/hosting_service/config.yaml b/AdaptixServer/extenders/hosting_service/config.yaml new file mode 100644 index 00000000..cca80d93 --- /dev/null +++ b/AdaptixServer/extenders/hosting_service/config.yaml @@ -0,0 +1,5 @@ +extender_type: "service" +extender_file: "service_hosting.so" +ax_file: "ax_config.axs" +service_name: "Hosting" +service_config: "" diff --git a/AdaptixServer/extenders/hosting_service/go.mod b/AdaptixServer/extenders/hosting_service/go.mod new file mode 100644 index 00000000..55d4822e --- /dev/null +++ b/AdaptixServer/extenders/hosting_service/go.mod @@ -0,0 +1,5 @@ +module adaptix_service_hosting + +go 1.25.4 + +require github.com/Adaptix-Framework/axc2 v1.2.0 diff --git a/AdaptixServer/extenders/hosting_service/go.sum b/AdaptixServer/extenders/hosting_service/go.sum new file mode 100644 index 00000000..8889bb84 --- /dev/null +++ b/AdaptixServer/extenders/hosting_service/go.sum @@ -0,0 +1,2 @@ +github.com/Adaptix-Framework/axc2 v1.2.0 h1:WYEg502NTTtX1tQJUz2AaC2dmm/bS/1L1iOHOQ5kEYA= +github.com/Adaptix-Framework/axc2 v1.2.0/go.mod h1:3oJyFeRVIql1RTsNa0meEqK3+P+6JTAMMjMdVyXhbaQ= diff --git a/AdaptixServer/extenders/hosting_service/pl_main.go b/AdaptixServer/extenders/hosting_service/pl_main.go new file mode 100644 index 00000000..911c2107 --- /dev/null +++ b/AdaptixServer/extenders/hosting_service/pl_main.go @@ -0,0 +1,652 @@ +package main + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/base64" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "net/http" + "regexp" + "strings" + "sync" + "time" + + adaptix "github.com/Adaptix-Framework/axc2" +) + +type Teamserver interface { + TsExtenderDataSave(extenderName string, key string, value []byte) error + TsExtenderDataLoad(extenderName string, key string) ([]byte, error) + TsExtenderDataDelete(extenderName string, key string) error + TsExtenderDataKeys(extenderName string) ([]string, error) + + TsEndpointRegisterPublicRaw(method string, path string, handler func(w http.ResponseWriter, r *http.Request)) error + TsEndpointUnregisterPublic(method string, path string) error + TsEndpointExistsPublic(method string, path string) bool + + TsServiceSendDataAll(service string, data string) + TsServiceSendDataClient(operator string, service string, data string) +} + +const ServiceName = "Hosting" +const ExtenderName = "hosting_service" + +// ============================================================================ +// Data Model +// ============================================================================ + +type HostedFile struct { + ID string `json:"id"` + Path string `json:"path"` + Filename string `json:"filename"` + MimeType string `json:"mime_type"` + ContentSize int `json:"content_size"` + Encrypted bool `json:"encrypted"` + EncKey string `json:"enc_key,omitempty"` + UAFilter string `json:"ua_filter"` + OneShot bool `json:"one_shot"` + MaxDownloads int `json:"max_downloads"` + Downloads int `json:"downloads"` + ExpiresAt string `json:"expires_at"` + Enabled bool `json:"enabled"` + CreatedBy string `json:"created_by"` + CreatedAt string `json:"created_at"` +} + +// ============================================================================ +// Service struct +// ============================================================================ + +type HostingService struct { + ts Teamserver + moduleDir string + mu sync.RWMutex + files map[string]*HostedFile +} + +var ( + Ts Teamserver + ModuleDir string + Service *HostingService +) + +// ============================================================================ +// Plugin Entry Points +// ============================================================================ + +func InitPlugin(ts any, moduleDir string, serviceConfig string) adaptix.PluginService { + Ts = ts.(Teamserver) + ModuleDir = moduleDir + + Service = &HostingService{ + ts: Ts, + moduleDir: moduleDir, + files: make(map[string]*HostedFile), + } + + Service.restoreFiles() + + return Service +} + +func (s *HostingService) Call(operator string, function string, args string) { + switch function { + + case "list": + s.HandleList(operator) + + case "add": + s.HandleAdd(operator, args) + + case "delete": + s.HandleDelete(operator, args) + + case "toggle": + s.HandleToggle(operator, args) + + case "copyurl": + s.HandleCopyURL(operator, args) + + case "host_payload": + s.HandleHostPayload(operator, args) + } +} + +// ============================================================================ +// Helpers: responses +// ============================================================================ + +func (s *HostingService) sendResponseAll(msgType string, data interface{}) { + resp := map[string]interface{}{ + "type": msgType, + "data": data, + } + jsonData, err := json.Marshal(resp) + if err != nil { + return + } + s.ts.TsServiceSendDataAll(ServiceName, string(jsonData)) +} + +func (s *HostingService) sendResponseClient(operator string, msgType string, data interface{}) { + resp := map[string]interface{}{ + "type": msgType, + "data": data, + } + jsonData, err := json.Marshal(resp) + if err != nil { + return + } + s.ts.TsServiceSendDataClient(operator, ServiceName, string(jsonData)) +} + +func (s *HostingService) sendEvent(eventType string, data interface{}) { + resp := map[string]interface{}{ + "type": "event", + "event": eventType, + "data": data, + } + jsonData, err := json.Marshal(resp) + if err != nil { + return + } + s.ts.TsServiceSendDataAll(ServiceName, string(jsonData)) +} + +func (s *HostingService) sendError(operator string, message string) { + resp := map[string]interface{}{ + "type": "error", + "message": message, + } + jsonData, err := json.Marshal(resp) + if err != nil { + return + } + s.ts.TsServiceSendDataClient(operator, ServiceName, string(jsonData)) +} + +// ============================================================================ +// Helpers: ID generation +// ============================================================================ + +func generateID() string { + b := make([]byte, 16) + rand.Read(b) + return hex.EncodeToString(b) +} + +func generatePath() string { + b := make([]byte, 8) + rand.Read(b) + return "/" + hex.EncodeToString(b) +} + +// ============================================================================ +// Handlers +// ============================================================================ + +func (s *HostingService) HandleList(operator string) { + s.mu.RLock() + defer s.mu.RUnlock() + + var list []HostedFile + for _, f := range s.files { + list = append(list, *f) + } + s.sendResponseClient(operator, "files", list) +} + +func (s *HostingService) HandleAdd(operator string, args string) { + var req struct { + Filename string `json:"filename"` + Content string `json:"content"` // base64 + Path string `json:"path"` + MimeType string `json:"mime_type"` + OneShot bool `json:"one_shot"` + Encrypted bool `json:"encrypted"` + UAFilter string `json:"ua_filter"` + MaxDownloads int `json:"max_downloads"` + ExpiresAt string `json:"expires_at"` + } + if err := json.Unmarshal([]byte(args), &req); err != nil { + s.sendError(operator, "Invalid request: "+err.Error()) + return + } + + if req.Content == "" { + s.sendError(operator, "No file content provided") + return + } + + content, err := base64.StdEncoding.DecodeString(req.Content) + if err != nil { + s.sendError(operator, "Invalid base64 content: "+err.Error()) + return + } + + s.addFile(operator, req.Filename, content, req.Path, req.MimeType, req.OneShot, req.Encrypted, req.UAFilter, req.MaxDownloads, req.ExpiresAt) +} + +func (s *HostingService) HandleHostPayload(operator string, args string) { + var req struct { + Filename string `json:"filename"` + Content string `json:"content"` // base64 + Path string `json:"path"` + MimeType string `json:"mime_type"` + } + if err := json.Unmarshal([]byte(args), &req); err != nil { + s.sendError(operator, "Invalid host_payload request: "+err.Error()) + return + } + + content, err := base64.StdEncoding.DecodeString(req.Content) + if err != nil { + s.sendError(operator, "Invalid base64 content: "+err.Error()) + return + } + + mime := req.MimeType + if mime == "" { + mime = "application/octet-stream" + } + + s.addFile(operator, req.Filename, content, req.Path, mime, false, false, "", 0, "") +} + +func (s *HostingService) addFile(operator, filename string, content []byte, path, mimeType string, oneShot, encrypted bool, uaFilter string, maxDownloads int, expiresAt string) { + id := generateID() + + if path == "" { + path = generatePath() + } + if !strings.HasPrefix(path, "/") { + path = "/" + path + } + if mimeType == "" { + mimeType = "application/octet-stream" + } + + // Check path collision + s.mu.RLock() + for _, f := range s.files { + if f.Path == path { + s.mu.RUnlock() + s.sendError(operator, "Path already in use: "+path) + return + } + } + s.mu.RUnlock() + + // Check if endpoint already registered externally + if s.ts.TsEndpointExistsPublic("GET", path) { + s.sendError(operator, "Endpoint already exists: "+path) + return + } + + var encKey string + storedContent := content + + if encrypted { + key := make([]byte, 32) + rand.Read(key) + encKey = hex.EncodeToString(key) + + encData, err := aesEncrypt(content, key) + if err != nil { + s.sendError(operator, "AES encryption failed: "+err.Error()) + return + } + storedContent = encData + } + + hf := &HostedFile{ + ID: id, + Path: path, + Filename: filename, + MimeType: mimeType, + ContentSize: len(content), + Encrypted: encrypted, + EncKey: encKey, + UAFilter: uaFilter, + OneShot: oneShot, + MaxDownloads: maxDownloads, + Downloads: 0, + ExpiresAt: expiresAt, + Enabled: true, + CreatedBy: operator, + CreatedAt: time.Now().Format("2006-01-02 15:04:05"), + } + + // Save metadata + metaJSON, err := json.Marshal(hf) + if err != nil { + s.sendError(operator, "Failed to marshal metadata: "+err.Error()) + return + } + if err := s.ts.TsExtenderDataSave(ExtenderName, "meta:"+id, metaJSON); err != nil { + s.sendError(operator, "Failed to save metadata: "+err.Error()) + return + } + + // Save content + if err := s.ts.TsExtenderDataSave(ExtenderName, "data:"+id, storedContent); err != nil { + s.sendError(operator, "Failed to save content: "+err.Error()) + return + } + + // Register endpoint + s.registerEndpoint(hf) + + s.mu.Lock() + s.files[id] = hf + s.mu.Unlock() + + // Broadcast updated file list + s.broadcastFiles() +} + +func (s *HostingService) HandleDelete(operator string, args string) { + var req struct { + ID string `json:"id"` + } + if err := json.Unmarshal([]byte(args), &req); err != nil { + s.sendError(operator, "Invalid request") + return + } + + s.mu.Lock() + hf, ok := s.files[req.ID] + if !ok { + s.mu.Unlock() + s.sendError(operator, "File not found") + return + } + + path := hf.Path + delete(s.files, req.ID) + s.mu.Unlock() + + // Unregister endpoint + s.ts.TsEndpointUnregisterPublic("GET", path) + + // Delete from storage + s.ts.TsExtenderDataDelete(ExtenderName, "meta:"+req.ID) + s.ts.TsExtenderDataDelete(ExtenderName, "data:"+req.ID) + + s.broadcastFiles() +} + +func (s *HostingService) HandleToggle(operator string, args string) { + var req struct { + ID string `json:"id"` + } + if err := json.Unmarshal([]byte(args), &req); err != nil { + s.sendError(operator, "Invalid request") + return + } + + s.mu.Lock() + hf, ok := s.files[req.ID] + if !ok { + s.mu.Unlock() + s.sendError(operator, "File not found") + return + } + + hf.Enabled = !hf.Enabled + s.mu.Unlock() + + // Save updated metadata + s.saveMeta(hf) + + s.broadcastFiles() +} + +func (s *HostingService) HandleCopyURL(operator string, args string) { + var req struct { + ID string `json:"id"` + } + if err := json.Unmarshal([]byte(args), &req); err != nil { + s.sendError(operator, "Invalid request") + return + } + + s.mu.RLock() + hf, ok := s.files[req.ID] + if !ok { + s.mu.RUnlock() + s.sendError(operator, "File not found") + return + } + path := hf.Path + s.mu.RUnlock() + + s.sendResponseClient(operator, "url", map[string]interface{}{ + "url": path, + }) +} + +// ============================================================================ +// HTTP Serving +// ============================================================================ + +func (s *HostingService) registerEndpoint(hf *HostedFile) { + fileID := hf.ID + path := hf.Path + + handler := func(w http.ResponseWriter, r *http.Request) { + s.mu.RLock() + f, ok := s.files[fileID] + if !ok { + s.mu.RUnlock() + http.NotFound(w, r) + return + } + + // 1. Check enabled + if !f.Enabled { + s.mu.RUnlock() + http.NotFound(w, r) + return + } + + // 2. Check expiration + if f.ExpiresAt != "" { + expiry, err := time.Parse("2006-01-02T15:04:05", f.ExpiresAt) + if err == nil && time.Now().After(expiry) { + s.mu.RUnlock() + http.NotFound(w, r) + return + } + } + + // 3. Check max downloads + if f.MaxDownloads > 0 && f.Downloads >= f.MaxDownloads { + s.mu.RUnlock() + http.NotFound(w, r) + return + } + + // 4. Check UA filter + if f.UAFilter != "" { + ua := r.UserAgent() + matched, err := regexp.MatchString(f.UAFilter, ua) + if err != nil || !matched { + s.mu.RUnlock() + http.NotFound(w, r) + return + } + } + + // Snapshot what we need before upgrading the lock + mimeType := f.MimeType + filename := f.Filename + encrypted := f.Encrypted + encKey := f.EncKey + oneShot := f.OneShot + s.mu.RUnlock() + + // Load content from storage + contentData, err := s.ts.TsExtenderDataLoad(ExtenderName, "data:"+fileID) + if err != nil || len(contentData) == 0 { + http.NotFound(w, r) + return + } + + // 5. Increment download count + s.mu.Lock() + f2, ok2 := s.files[fileID] + if ok2 { + f2.Downloads++ + if oneShot { + f2.Enabled = false + } + } + s.mu.Unlock() + + // 7. Serve content + w.Header().Set("Content-Type", mimeType) + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) + w.Header().Set("Content-Length", fmt.Sprintf("%d", len(contentData))) + w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0") + + // 8. If encrypted, add key header + if encrypted && encKey != "" { + w.Header().Set("X-Enc-Key", encKey) + } + + w.Write(contentData) + + // 9. Broadcast download event + remoteIP := getRemoteIP(r) + userAgent := r.UserAgent() + go func() { + s.sendEvent("download", map[string]interface{}{ + "file_id": fileID, + "filename": filename, + "path": path, + "remote_ip": remoteIP, + "user_agent": userAgent, + "time": time.Now().Format("2006-01-02 15:04:05"), + }) + + // 10. Persist updated metadata + s.mu.RLock() + f3, ok3 := s.files[fileID] + if ok3 { + s.saveMeta(f3) + } + s.mu.RUnlock() + + // Broadcast updated files list + s.broadcastFiles() + }() + } + + s.ts.TsEndpointRegisterPublicRaw("GET", path, handler) +} + +func getRemoteIP(r *http.Request) string { + if xff := r.Header.Get("X-Forwarded-For"); xff != "" { + parts := strings.Split(xff, ",") + return strings.TrimSpace(parts[0]) + } + if xri := r.Header.Get("X-Real-IP"); xri != "" { + return xri + } + return strings.Split(r.RemoteAddr, ":")[0] +} + +// ============================================================================ +// Persistence +// ============================================================================ + +func (s *HostingService) saveMeta(hf *HostedFile) { + metaJSON, err := json.Marshal(hf) + if err != nil { + return + } + s.ts.TsExtenderDataSave(ExtenderName, "meta:"+hf.ID, metaJSON) +} + +func (s *HostingService) restoreFiles() { + keys, err := s.ts.TsExtenderDataKeys(ExtenderName) + if err != nil { + return + } + + for _, key := range keys { + if !strings.HasPrefix(key, "meta:") { + continue + } + + data, err := s.ts.TsExtenderDataLoad(ExtenderName, key) + if err != nil { + continue + } + + var hf HostedFile + if json.Unmarshal(data, &hf) != nil { + continue + } + + // Verify content exists + id := strings.TrimPrefix(key, "meta:") + _, err = s.ts.TsExtenderDataLoad(ExtenderName, "data:"+id) + if err != nil { + continue + } + + s.files[hf.ID] = &hf + + // Re-register endpoint + s.registerEndpoint(&hf) + } +} + +func (s *HostingService) broadcastFiles() { + s.mu.RLock() + defer s.mu.RUnlock() + + var list []HostedFile + for _, f := range s.files { + list = append(list, *f) + } + s.sendResponseAll("files", list) +} + +// ============================================================================ +// AES-256-CBC Encryption +// ============================================================================ + +func aesEncrypt(plaintext []byte, key []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + // PKCS7 padding + blockSize := block.BlockSize() + padding := blockSize - len(plaintext)%blockSize + padtext := bytes.Repeat([]byte{byte(padding)}, padding) + plaintext = append(plaintext, padtext...) + + // Random IV + iv := make([]byte, blockSize) + if _, err := io.ReadFull(rand.Reader, iv); err != nil { + return nil, err + } + + // Encrypt + mode := cipher.NewCBCEncrypter(block, iv) + ciphertext := make([]byte, len(plaintext)) + mode.CryptBlocks(ciphertext, plaintext) + + // Prepend IV + return append(iv, ciphertext...), nil +} diff --git a/AdaptixServer/go.work b/AdaptixServer/go.work index 9ac15ecb..78401227 100644 --- a/AdaptixServer/go.work +++ b/AdaptixServer/go.work @@ -9,4 +9,5 @@ use ( ./extenders/beacon_listener_tcp ./extenders/gopher_agent ./extenders/gopher_listener_tcp + ./extenders/hosting_service ) diff --git a/AdaptixServer/profile.yaml b/AdaptixServer/profile.yaml index d303f712..776a92a2 100644 --- a/AdaptixServer/profile.yaml +++ b/AdaptixServer/profile.yaml @@ -17,6 +17,7 @@ Teamserver: - "extenders/beacon_agent/config.yaml" - "extenders/gopher_listener_tcp/config.yaml" - "extenders/gopher_agent/config.yaml" + - "extenders/hosting_service/config.yaml" axscripts: # - "Extension-Kit/extension-kit.axs" access_token_live_hours: 12