mirror of
https://github.com/luckfox-eng29/kvm.git
synced 2026-05-27 16:45:08 +02:00
390 lines
10 KiB
Go
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,
|
|
})
|
|
}
|