mirror of
https://github.com/luckfox-eng29/kvm.git
synced 2026-06-17 12:06:53 +02:00
Update App version to 0.1.1
Signed-off-by: luckfox-eng29 <eng29@luckfox.com>
This commit is contained in:
234
web.go
234
web.go
@@ -12,6 +12,7 @@ import (
|
||||
"net/http/pprof"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"kvm/internal/logging"
|
||||
@@ -85,10 +86,16 @@ func setupRouter() *gin.Engine {
|
||||
// By enabling caching, we ensure that pre-loaded images are stored in the browser cache
|
||||
// This allows for a smoother enter animation and improved user experience on the welcome screen
|
||||
r.Use(func(c *gin.Context) {
|
||||
c.Header("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
c.Header("Pragma", "no-cache")
|
||||
c.Header("Expires", "0")
|
||||
|
||||
if strings.HasPrefix(c.Request.URL.Path, "/static/") {
|
||||
ext := filepath.Ext(c.Request.URL.Path)
|
||||
if ext == ".jpg" || ext == ".jpeg" || ext == ".png" || ext == ".gif" || ext == ".webp" {
|
||||
c.Header("Cache-Control", "public, max-age=300") // Cache for 5 minutes
|
||||
c.Header("Pragma", "")
|
||||
c.Header("Expires", "")
|
||||
}
|
||||
}
|
||||
c.Next()
|
||||
@@ -160,6 +167,10 @@ func setupRouter() *gin.Engine {
|
||||
protected.POST("/storage/upload", handleUploadHttp)
|
||||
protected.GET("/storage/download", handleDownloadHttp)
|
||||
protected.GET("/storage/sd-download", handleSDDownloadHttp)
|
||||
protected.POST("/api/rpc", handleRpcRequest)
|
||||
protected.GET("/terminal/ws", handleTerminalWS)
|
||||
protected.GET("/serial/ws", handleSerialWS)
|
||||
protected.GET("/video/stream", handleVideoStream)
|
||||
}
|
||||
|
||||
// Catch-all route for SPA
|
||||
@@ -174,9 +185,17 @@ func setupRouter() *gin.Engine {
|
||||
return r
|
||||
}
|
||||
|
||||
// TODO: support multiple sessions?
|
||||
var currentSession *Session
|
||||
|
||||
var (
|
||||
currentHTTPSessionID string
|
||||
httpSessionToNotify string
|
||||
httpSessionMu sync.Mutex
|
||||
httpSessionLockVersion int64
|
||||
httpSessionSeenVersions map[string]int64
|
||||
invalidHTTPSessions map[string]bool
|
||||
)
|
||||
|
||||
func handleWebRTCSession(c *gin.Context) {
|
||||
var req WebRTCSessionRequest
|
||||
|
||||
@@ -421,6 +440,14 @@ func handleLogin(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
ip := c.ClientIP()
|
||||
if allowed, wait := CheckRateLimit(ip); !allowed {
|
||||
c.JSON(http.StatusTooManyRequests, gin.H{
|
||||
"error": fmt.Sprintf("Too many failed attempts. Please try again in %s", wait.Round(time.Second)),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var req LoginRequest
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
@@ -430,14 +457,17 @@ func handleLogin(c *gin.Context) {
|
||||
|
||||
err := bcrypt.CompareHashAndPassword([]byte(config.HashedPassword), []byte(req.Password))
|
||||
if err != nil {
|
||||
RecordFailure(ip)
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid password"})
|
||||
return
|
||||
}
|
||||
|
||||
RecordSuccess(ip)
|
||||
|
||||
config.LocalAuthToken = uuid.New().String()
|
||||
|
||||
// Set the cookie
|
||||
c.SetCookie("authToken", config.LocalAuthToken, 7*24*60*60, "/", "", false, true)
|
||||
// Set the cookie (Session cookie, expires on browser close)
|
||||
c.SetCookie("authToken", config.LocalAuthToken, 0, "/", "", false, true)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Login successful"})
|
||||
}
|
||||
@@ -505,12 +535,21 @@ func basicAuthProtectedMiddleware(requireDeveloperMode bool) gin.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
ip := c.ClientIP()
|
||||
if allowed, wait := CheckRateLimit(ip); !allowed {
|
||||
sendErrorJsonThenAbort(c, http.StatusTooManyRequests, fmt.Sprintf("Too many failed attempts. Please try again in %s", wait.Round(time.Second)))
|
||||
return
|
||||
}
|
||||
|
||||
err := bcrypt.CompareHashAndPassword([]byte(config.HashedPassword), []byte(password))
|
||||
if err != nil {
|
||||
RecordFailure(ip)
|
||||
sendErrorJsonThenAbort(c, http.StatusUnauthorized, "Invalid password")
|
||||
return
|
||||
}
|
||||
|
||||
RecordSuccess(ip)
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
@@ -575,8 +614,8 @@ func handleCreatePassword(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Set the cookie
|
||||
c.SetCookie("authToken", config.LocalAuthToken, 7*24*60*60, "/", "", false, true)
|
||||
// Set the cookie (Session cookie, expires on browser close)
|
||||
c.SetCookie("authToken", config.LocalAuthToken, 0, "/", "", false, true)
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{"message": "Password set successfully"})
|
||||
}
|
||||
@@ -618,8 +657,8 @@ func handleUpdatePassword(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Set the cookie
|
||||
c.SetCookie("authToken", config.LocalAuthToken, 7*24*60*60, "/", "", false, true)
|
||||
// Set the cookie (Session cookie, expires on browser close)
|
||||
c.SetCookie("authToken", config.LocalAuthToken, 0, "/", "", false, true)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Password updated successfully"})
|
||||
}
|
||||
@@ -719,8 +758,8 @@ func handleSetup(c *gin.Context) {
|
||||
config.HashedPassword = string(hashedPassword)
|
||||
config.LocalAuthToken = uuid.New().String()
|
||||
|
||||
// Set the cookie
|
||||
c.SetCookie("authToken", config.LocalAuthToken, 7*24*60*60, "/", "", false, true)
|
||||
// Set the cookie (Session cookie, expires on browser close)
|
||||
c.SetCookie("authToken", config.LocalAuthToken, 0, "/", "", false, true)
|
||||
} else {
|
||||
// For noPassword mode, ensure the password field is empty
|
||||
config.HashedPassword = ""
|
||||
@@ -735,3 +774,180 @@ func handleSetup(c *gin.Context) {
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Device setup completed successfully"})
|
||||
}
|
||||
|
||||
func handleRpcRequest(c *gin.Context) {
|
||||
var req JSONRPCRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid JSON RPC request"})
|
||||
return
|
||||
}
|
||||
sessionID := c.GetHeader("X-Session-ID")
|
||||
if sessionID == "" {
|
||||
var err error
|
||||
sessionID, err = c.Cookie("httpSessionId")
|
||||
if err != nil || sessionID == "" {
|
||||
sessionID = uuid.New().String()
|
||||
c.SetCookie("httpSessionId", sessionID, 7*24*60*60, "/", "", false, false)
|
||||
}
|
||||
}
|
||||
|
||||
var event *JSONRPCEvent
|
||||
|
||||
httpSessionMu.Lock()
|
||||
if httpSessionSeenVersions == nil {
|
||||
httpSessionSeenVersions = make(map[string]int64)
|
||||
}
|
||||
|
||||
if invalidHTTPSessions == nil {
|
||||
invalidHTTPSessions = make(map[string]bool)
|
||||
}
|
||||
|
||||
invalid := false
|
||||
|
||||
if req.Method == "confirmOtherSession" {
|
||||
httpSessionLockVersion++
|
||||
currentHTTPSessionID = sessionID
|
||||
httpSessionToNotify = ""
|
||||
httpSessionSeenVersions[sessionID] = httpSessionLockVersion
|
||||
|
||||
for id := range httpSessionSeenVersions {
|
||||
if id == sessionID {
|
||||
delete(invalidHTTPSessions, id)
|
||||
continue
|
||||
}
|
||||
invalidHTTPSessions[id] = true
|
||||
}
|
||||
|
||||
//logger.Info().
|
||||
// Str("sessionID", sessionID).
|
||||
// Str("currentHTTPSessionID", currentHTTPSessionID).
|
||||
// Str("httpSessionToNotify", httpSessionToNotify).
|
||||
// Int64("httpSessionLockVersion", httpSessionLockVersion).
|
||||
// Str("rpcMethod", req.Method).
|
||||
// Msg("handleRpcRequest confirmOtherSession")
|
||||
} else {
|
||||
if invalidHTTPSessions[sessionID] {
|
||||
invalid = true
|
||||
event = &JSONRPCEvent{
|
||||
JSONRPC: "2.0",
|
||||
Method: "sessionInvalidated",
|
||||
}
|
||||
} else {
|
||||
if _, ok := httpSessionSeenVersions[sessionID]; !ok {
|
||||
httpSessionSeenVersions[sessionID] = httpSessionLockVersion
|
||||
}
|
||||
|
||||
//logger.Info().
|
||||
// Str("sessionID", sessionID).
|
||||
// Str("currentHTTPSessionID", currentHTTPSessionID).
|
||||
// Str("httpSessionToNotify", httpSessionToNotify).
|
||||
// Int64("httpSessionLockVersion", httpSessionLockVersion).
|
||||
// Str("rpcMethod", req.Method).
|
||||
// Msg("handleRpcRequest before session check")
|
||||
|
||||
if currentHTTPSessionID == "" {
|
||||
currentHTTPSessionID = sessionID
|
||||
} else if sessionID == httpSessionToNotify {
|
||||
event = &JSONRPCEvent{
|
||||
JSONRPC: "2.0",
|
||||
Method: "otherSessionConnected",
|
||||
}
|
||||
httpSessionToNotify = ""
|
||||
} else if sessionID != currentHTTPSessionID {
|
||||
if version, ok := httpSessionSeenVersions[sessionID]; ok && version >= httpSessionLockVersion {
|
||||
httpSessionToNotify = currentHTTPSessionID
|
||||
currentHTTPSessionID = sessionID
|
||||
}
|
||||
}
|
||||
|
||||
//logger.Info().
|
||||
// Str("sessionID", sessionID).
|
||||
// Str("currentHTTPSessionID", currentHTTPSessionID).
|
||||
// Str("httpSessionToNotify", httpSessionToNotify).
|
||||
// Int64("httpSessionLockVersion", httpSessionLockVersion).
|
||||
// Bool("hasEvent", event != nil).
|
||||
// Str("rpcMethod", req.Method).
|
||||
// Msg("handleRpcRequest after session check")
|
||||
}
|
||||
}
|
||||
httpSessionMu.Unlock()
|
||||
|
||||
if invalid {
|
||||
response := JSONRPCResponse{
|
||||
JSONRPC: "2.0",
|
||||
Error: map[string]interface{}{
|
||||
"code": -32001,
|
||||
"message": "Session invalidated",
|
||||
},
|
||||
ID: req.ID,
|
||||
}
|
||||
|
||||
if event != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"response": response,
|
||||
"event": event,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
return
|
||||
}
|
||||
|
||||
response, _ := DispatchRPCRequest(req)
|
||||
|
||||
if event != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"response": response,
|
||||
"event": event,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
func handleVideoStream(c *gin.Context) {
|
||||
logger.Info().Msg("HTTP video stream request received")
|
||||
|
||||
c.Header("Content-Type", "video/x-h264")
|
||||
c.Writer.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
c.Writer.Header().Set("Cache-Control", "no-cache")
|
||||
c.Writer.Header().Set("Connection", "keep-alive")
|
||||
|
||||
// Flush headers immediately so client knows the connection is established
|
||||
c.Writer.Flush()
|
||||
logger.Info().Msg("HTTP video stream headers flushed, waiting for video data")
|
||||
|
||||
id, ch := videoBroadcaster.Subscribe()
|
||||
logger.Info().Str("subscriber_id", id).Msg("subscribed to video broadcaster")
|
||||
defer func() {
|
||||
videoBroadcaster.Unsubscribe(id)
|
||||
logger.Info().Str("subscriber_id", id).Msg("unsubscribed from video broadcaster")
|
||||
}()
|
||||
|
||||
ctx := c.Request.Context()
|
||||
frameCount := 0
|
||||
|
||||
for {
|
||||
select {
|
||||
case data, ok := <-ch:
|
||||
if !ok {
|
||||
logger.Info().Int("total_frames", frameCount).Msg("video broadcaster channel closed")
|
||||
return
|
||||
}
|
||||
frameCount++
|
||||
if frameCount == 1 {
|
||||
logger.Info().Int("size", len(data)).Msg("first video frame received")
|
||||
}
|
||||
if _, err := c.Writer.Write(data); err != nil {
|
||||
logger.Warn().Err(err).Int("total_frames", frameCount).Msg("error writing video data")
|
||||
return
|
||||
}
|
||||
c.Writer.Flush()
|
||||
case <-ctx.Done():
|
||||
logger.Info().Int("total_frames", frameCount).Msg("client disconnected")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user