Files
kvm/api.go
2026-05-16 16:39:54 +08:00

390 lines
10 KiB
Go

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,
})
}