7be99f8e91
- agent+proto+backend: transfer service status - agent: fix returning empty message on nonzero exit status - backend: refactor collector+commander and handlers dependent on them: implement agent accounting via grpc stats handler
200 lines
5.0 KiB
Go
200 lines
5.0 KiB
Go
package handlers
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"os/exec"
|
|
"strconv"
|
|
|
|
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/grpcsrv/commander"
|
|
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/models"
|
|
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/service"
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
type JobsHandlers struct {
|
|
tracker *commander.ConnTracker
|
|
svc *service.ScriptService
|
|
whereami string
|
|
}
|
|
|
|
func NewJobsHandlers(tracker *commander.ConnTracker, svc *service.ScriptService, whereami string) JobsHandlers {
|
|
return JobsHandlers{tracker: tracker, svc: svc, whereami: whereami}
|
|
}
|
|
|
|
// AddJobIn is the request body for creating a job.
|
|
type AddJobIn struct {
|
|
Command string `json:"command" binding:"required"`
|
|
InterpreterID int64 `json:"interpreter_id"`
|
|
Stdin *string `json:"stdin"`
|
|
AgentID string `json:"agent_id" binding:"required"`
|
|
}
|
|
|
|
// AddJobOut is the response body for a submitted job.
|
|
type AddJobOut struct {
|
|
ID int64 `json:"id"`
|
|
Command []string `json:"command"`
|
|
WaitURL string `json:"wait_url"`
|
|
}
|
|
|
|
// JobResult is the response body for a completed job.
|
|
type JobResult struct {
|
|
ID int64 `json:"id"`
|
|
Command []string `json:"command"`
|
|
Stdin *string `json:"stdin"`
|
|
Stdout string `json:"stdout"`
|
|
Stderr string `json:"stderr"`
|
|
Status int32 `json:"status"`
|
|
}
|
|
|
|
// WaitJobIn is the request body for waiting on a job.
|
|
type WaitJobIn struct {
|
|
AgentID string `json:"agent_id" binding:"required"`
|
|
}
|
|
|
|
// AddJob submits a job to an agent and returns a wait_url for the result.
|
|
// @Summary Submit a job to an agent
|
|
// @Description Sends a command to the specified agent and returns a URL to wait for the result
|
|
// @Tags jobs
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param body body AddJobIn true "Job request"
|
|
// @Success 201 {object} AddJobOut
|
|
// @Router /jobs [post]
|
|
func (h *JobsHandlers) AddJob(c *gin.Context) {
|
|
var in AddJobIn
|
|
if err := c.Bind(&in); err != nil {
|
|
c.Error(err)
|
|
return
|
|
}
|
|
|
|
agent, ok := h.tracker.GetAgent(in.AgentID)
|
|
if !ok {
|
|
c.Status(http.StatusNotFound)
|
|
c.Error(fmt.Errorf("agent not found"))
|
|
return
|
|
}
|
|
|
|
command, err := resolveCommand(c, h.svc, in.InterpreterID, in.Command)
|
|
if err != nil {
|
|
c.Error(err)
|
|
return
|
|
}
|
|
|
|
jid, err := agent.AddJob(models.JobForInsert{
|
|
Command: command,
|
|
Stdin: in.Stdin,
|
|
})
|
|
if err != nil {
|
|
c.Error(err)
|
|
return
|
|
}
|
|
|
|
waitURL := fmt.Sprintf("%s/api/v1/jobs/%d/wait", h.whereami, jid)
|
|
|
|
c.JSON(http.StatusCreated, AddJobOut{
|
|
ID: jid,
|
|
Command: command,
|
|
WaitURL: waitURL,
|
|
})
|
|
}
|
|
|
|
// WaitJob waits for a submitted job to complete (long-poll).
|
|
// If the job is already done, returns immediately.
|
|
// @Summary Wait for job result
|
|
// @Description Long-polls for a job result. Returns immediately if the job is already finished.
|
|
// @Tags jobs
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param id path int true "Job ID"
|
|
// @Param body body WaitJobIn true "Agent reference"
|
|
// @Success 200 {object} JobResult
|
|
// @Failure 400 {object} map[string]string
|
|
// @Failure 404 {object} map[string]string
|
|
// @Router /jobs/{id}/wait [post]
|
|
func (h *JobsHandlers) WaitJob(c *gin.Context) {
|
|
jid, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid job id"})
|
|
return
|
|
}
|
|
|
|
var in WaitJobIn
|
|
if err := c.Bind(&in); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
|
|
return
|
|
}
|
|
|
|
agent, ok := h.tracker.GetAgent(in.AgentID)
|
|
if !ok {
|
|
c.Status(http.StatusNotFound)
|
|
c.Error(fmt.Errorf("agent not found"))
|
|
return
|
|
}
|
|
|
|
job, err := agent.WaitJob(jid)
|
|
if err != nil {
|
|
c.Error(err)
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, JobResult{
|
|
ID: job.ID,
|
|
Command: job.Command,
|
|
Stdin: job.Stdin,
|
|
Stdout: job.Stdout,
|
|
Stderr: job.Stderr,
|
|
Status: job.Status,
|
|
})
|
|
}
|
|
|
|
func resolveCommand(c *gin.Context, svc *service.ScriptService, interpID int64, cmd string) ([]string, error) {
|
|
if interpID == 0 {
|
|
return []string{"sh", "-c", cmd}, nil
|
|
}
|
|
|
|
command, err := svc.ResolveCommand(c.Request.Context(), interpID, cmd)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return command, nil
|
|
}
|
|
|
|
// @Summary Check command path
|
|
// @Description Validates that a command binary exists on the system
|
|
// @Tags jobs
|
|
// @Accept json
|
|
// @Param body body CheckCmdIn true "Command to check"
|
|
// @Success 200 {object} CheckCmdOut
|
|
// @Failure 404 {object} map[string]string
|
|
// @Router /jobs/check_cmd [post]
|
|
func (h *JobsHandlers) CheckCmd(c *gin.Context) {
|
|
var in struct {
|
|
Command string `json:"command" binding:"required"`
|
|
}
|
|
if err := c.Bind(&in); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request"})
|
|
return
|
|
}
|
|
|
|
if _, err := exec.LookPath(in.Command); err != nil {
|
|
if errors.Is(err, exec.ErrNotFound) {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "command not found"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, CheckCmdOut{Exists: true})
|
|
}
|
|
|
|
type CheckCmdIn struct {
|
|
Command string `json:"command" binding:"required" example:"bash"`
|
|
}
|
|
type CheckCmdOut struct {
|
|
Exists bool `json:"exists"`
|
|
}
|