mirror of
https://github.com/luckfox-eng29/kvm.git
synced 2026-05-28 00:51:22 +02:00
feat: add support for MCP service and CLI subcommands
Signed-off-by: luckfox-eng29 <eng29@luckfox.com>
This commit is contained in:
389
api.go
Normal file
389
api.go
Normal file
@@ -0,0 +1,389 @@
|
||||
package kvm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func StartAPIServer(port int) {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
r := gin.New()
|
||||
r.Use(gin.Recovery())
|
||||
r.Use(corsMiddleware())
|
||||
r.Use(apiKeyAuthMiddleware(config.APIKey))
|
||||
|
||||
// Health check (no auth required)
|
||||
r.GET("/health", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{
|
||||
"status": "ok",
|
||||
"device": GetDeviceID(),
|
||||
"version": builtAppVersion,
|
||||
})
|
||||
})
|
||||
|
||||
// API routes
|
||||
api := r.Group("/api/lan")
|
||||
{
|
||||
// Mouse
|
||||
api.POST("/mouse/absolute", handleAPIMouseAbsolute)
|
||||
api.POST("/mouse/relative", handleAPIMouseRelative)
|
||||
api.POST("/mouse/click", handleAPIMouseClick)
|
||||
api.POST("/mouse/scroll", handleAPIMouseScroll)
|
||||
|
||||
// Keyboard
|
||||
api.POST("/keyboard/key", handleAPIKeyboardKey)
|
||||
api.POST("/keyboard/combo", handleAPIKeyboardCombo)
|
||||
api.POST("/keyboard/type", handleAPIKeyboardType)
|
||||
|
||||
// Capture
|
||||
api.GET("/capture", handleAPICapture)
|
||||
|
||||
// Status
|
||||
api.GET("/status", handleAPIStatus)
|
||||
api.GET("/video/state", handleAPIVideoState)
|
||||
}
|
||||
|
||||
addr := fmt.Sprintf(":%d", port)
|
||||
logger.Info().Str("addr", addr).Msg("Starting LAN API server")
|
||||
if err := r.Run(addr); err != nil {
|
||||
logger.Error().Err(err).Msg("LAN API server failed")
|
||||
}
|
||||
}
|
||||
|
||||
// === Middleware ===
|
||||
|
||||
func corsMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Header("Access-Control-Allow-Origin", "*")
|
||||
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
c.Header("Access-Control-Allow-Headers", "Origin, Content-Type, Accept, Authorization")
|
||||
if c.Request.Method == "OPTIONS" {
|
||||
c.AbortWithStatus(204)
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func apiKeyAuthMiddleware(expectedKey string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// Skip auth for health endpoint
|
||||
if c.Request.URL.Path == "/health" {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// Skip auth for localhost
|
||||
remoteAddr := c.Request.RemoteAddr
|
||||
if strings.HasPrefix(remoteAddr, "127.0.0.1:") ||
|
||||
strings.HasPrefix(remoteAddr, "[::1]:") {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// If no API key configured, reject LAN requests
|
||||
if expectedKey == "" {
|
||||
c.AbortWithStatusJSON(401, gin.H{"error": "API key not configured"})
|
||||
return
|
||||
}
|
||||
|
||||
auth := c.GetHeader("Authorization")
|
||||
var key string
|
||||
if _, err := fmt.Sscanf(auth, "Bearer %s", &key); err != nil {
|
||||
c.AbortWithStatusJSON(401, gin.H{"error": "missing or invalid authorization"})
|
||||
return
|
||||
}
|
||||
if !strings.EqualFold(key, expectedKey) {
|
||||
c.AbortWithStatusJSON(401, gin.H{"error": "invalid api key"})
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// === API Handlers: Mouse ===
|
||||
|
||||
func handleAPIMouseAbsolute(c *gin.Context) {
|
||||
var req struct {
|
||||
X int `json:"x" binding:"required"`
|
||||
Y int `json:"y" binding:"required"`
|
||||
Buttons uint8 `json:"buttons"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
_, err := callRPCHandler(rpcHandlers["absMouseReport"], map[string]interface{}{
|
||||
"x": req.X, "y": req.Y, "buttons": req.Buttons,
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "x": req.X, "y": req.Y})
|
||||
}
|
||||
|
||||
func handleAPIMouseRelative(c *gin.Context) {
|
||||
var req struct {
|
||||
Dx int8 `json:"dx" binding:"required"`
|
||||
Dy int8 `json:"dy" binding:"required"`
|
||||
Buttons uint8 `json:"buttons"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
_, err := callRPCHandler(rpcHandlers["relMouseReport"], map[string]interface{}{
|
||||
"dx": req.Dx, "dy": req.Dy, "buttons": req.Buttons,
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "dx": req.Dx, "dy": req.Dy})
|
||||
}
|
||||
|
||||
func handleAPIMouseClick(c *gin.Context) {
|
||||
var req struct {
|
||||
Button string `json:"button" binding:"required"`
|
||||
Double bool `json:"double"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
var buttons uint8
|
||||
switch req.Button {
|
||||
case "left":
|
||||
buttons = 1
|
||||
case "right":
|
||||
buttons = 2
|
||||
case "middle":
|
||||
buttons = 4
|
||||
default:
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("unknown button: %s", req.Button)})
|
||||
return
|
||||
}
|
||||
clickCount := 1
|
||||
if req.Double {
|
||||
clickCount = 2
|
||||
}
|
||||
for i := 0; i < clickCount; i++ {
|
||||
_, err := callRPCHandler(rpcHandlers["absMouseReport"], map[string]interface{}{
|
||||
"x": 0, "y": 0, "buttons": buttons,
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
_, err = callRPCHandler(rpcHandlers["absMouseReport"], map[string]interface{}{
|
||||
"x": 0, "y": 0, "buttons": 0,
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "button": req.Button, "clicks": clickCount})
|
||||
}
|
||||
|
||||
func handleAPIMouseScroll(c *gin.Context) {
|
||||
var req struct {
|
||||
Delta int8 `json:"delta" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
_, err := callRPCHandler(rpcHandlers["wheelReport"], map[string]interface{}{
|
||||
"wheelY": req.Delta,
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "delta": req.Delta})
|
||||
}
|
||||
|
||||
// === API Handlers: Keyboard ===
|
||||
|
||||
func handleAPIKeyboardKey(c *gin.Context) {
|
||||
var req struct {
|
||||
Key string `json:"key" binding:"required"`
|
||||
Action string `json:"action"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
keyCode, ok := keyNameToCode[req.Key]
|
||||
if !ok {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("unknown key: %s", req.Key)})
|
||||
return
|
||||
}
|
||||
action := strings.ToLower(req.Action)
|
||||
if action == "" {
|
||||
action = "press"
|
||||
}
|
||||
switch action {
|
||||
case "press":
|
||||
_, err := callRPCHandler(rpcHandlers["keyboardReport"], map[string]interface{}{
|
||||
"modifier": 0, "keys": []uint8{keyCode},
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
_, err = callRPCHandler(rpcHandlers["keyboardReport"], map[string]interface{}{
|
||||
"modifier": 0, "keys": []uint8{},
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
case "down":
|
||||
_, err := callRPCHandler(rpcHandlers["keyboardReport"], map[string]interface{}{
|
||||
"modifier": 0, "keys": []uint8{keyCode},
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
case "up":
|
||||
_, err := callRPCHandler(rpcHandlers["keyboardReport"], map[string]interface{}{
|
||||
"modifier": 0, "keys": []uint8{},
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
default:
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("unknown action: %s", req.Action)})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "key": req.Key, "action": action})
|
||||
}
|
||||
|
||||
func handleAPIKeyboardCombo(c *gin.Context) {
|
||||
var req struct {
|
||||
Keys []string `json:"keys" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
var keys []uint8
|
||||
var modifier uint8
|
||||
for _, keyName := range req.Keys {
|
||||
switch strings.ToLower(keyName) {
|
||||
case "ctrl", "control":
|
||||
modifier |= 0x01
|
||||
continue
|
||||
case "shift":
|
||||
modifier |= 0x02
|
||||
continue
|
||||
case "alt":
|
||||
modifier |= 0x04
|
||||
continue
|
||||
case "meta", "win", "cmd":
|
||||
modifier |= 0x08
|
||||
continue
|
||||
}
|
||||
keyCode, ok := keyNameToCode[keyName]
|
||||
if !ok {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("unknown key: %s", keyName)})
|
||||
return
|
||||
}
|
||||
keys = append(keys, keyCode)
|
||||
}
|
||||
_, err := callRPCHandler(rpcHandlers["keyboardReport"], map[string]interface{}{
|
||||
"modifier": modifier, "keys": keys,
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
_, err = callRPCHandler(rpcHandlers["keyboardReport"], map[string]interface{}{
|
||||
"modifier": 0, "keys": []uint8{},
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "keys": req.Keys})
|
||||
}
|
||||
|
||||
func handleAPIKeyboardType(c *gin.Context) {
|
||||
var req struct {
|
||||
Text string `json:"text" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
for _, char := range req.Text {
|
||||
keyCode, modifier, ok := charToKeyCode(uint8(char))
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
_, err := callRPCHandler(rpcHandlers["keyboardReport"], map[string]interface{}{
|
||||
"modifier": modifier, "keys": []uint8{keyCode},
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
_, err = callRPCHandler(rpcHandlers["keyboardReport"], map[string]interface{}{
|
||||
"modifier": 0, "keys": []uint8{},
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "text": req.Text})
|
||||
}
|
||||
|
||||
// === API Handlers: Capture ===
|
||||
|
||||
func handleAPICapture(c *gin.Context) {
|
||||
data, err := captureScreenshot("jpeg")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.Data(http.StatusOK, "image/jpeg", data)
|
||||
}
|
||||
|
||||
// === API Handlers: Status ===
|
||||
|
||||
func handleAPIStatus(c *gin.Context) {
|
||||
state := lastVideoState
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"video": gin.H{
|
||||
"ready": state.Ready,
|
||||
"width": state.Width,
|
||||
"height": state.Height,
|
||||
"fps": state.FramePerSecond,
|
||||
"error": state.Error,
|
||||
},
|
||||
"device": gin.H{
|
||||
"name": "PicoKVM",
|
||||
"version": builtAppVersion,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func handleAPIVideoState(c *gin.Context) {
|
||||
state := lastVideoState
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"ready": state.Ready,
|
||||
"width": state.Width,
|
||||
"height": state.Height,
|
||||
"fps": state.FramePerSecond,
|
||||
"error": state.Error,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user