anonpenguin23 fd87eec476 feat(security): add manifest signing, TLS TOFU, refresh token migration
- Invalidate plaintext refresh tokens (migration 019)
- Add `--sign` flag to `orama build` for rootwallet manifest signing
- Add `--ca-fingerprint` TOFU verification for production joins/invites
- Save cluster secrets from join (RQLite auth, Olric key, IPFS peers)
- Add RQLite auth config fields
2026-02-28 15:40:43 +02:00

130 lines
3.2 KiB
Go

package serverless
import (
"context"
"encoding/json"
"net/http"
"net/url"
"strings"
"time"
"github.com/DeBrosOfficial/network/pkg/serverless"
"github.com/google/uuid"
"github.com/gorilla/websocket"
"go.uber.org/zap"
)
// checkWSOrigin validates WebSocket origins against the request's Host header.
// Non-browser clients (no Origin) are allowed. Browser clients must match the host.
func checkWSOrigin(r *http.Request) bool {
origin := r.Header.Get("Origin")
if origin == "" {
return true
}
host := r.Host
if host == "" {
return false
}
// Strip port from host if present
if idx := strings.LastIndex(host, ":"); idx != -1 {
host = host[:idx]
}
parsed, err := url.Parse(origin)
if err != nil {
return false
}
originHost := parsed.Hostname()
return originHost == host || strings.HasSuffix(originHost, "."+host)
}
// HandleWebSocket handles WebSocket connections for function streaming.
// It upgrades HTTP connections to WebSocket and manages bi-directional communication
// for real-time function invocation and streaming responses.
func (h *ServerlessHandlers) HandleWebSocket(w http.ResponseWriter, r *http.Request, name string, version int) {
namespace := r.URL.Query().Get("namespace")
if namespace == "" {
namespace = h.getNamespaceFromRequest(r)
}
if namespace == "" {
http.Error(w, "namespace required", http.StatusBadRequest)
return
}
// Upgrade to WebSocket
upgrader := websocket.Upgrader{
CheckOrigin: checkWSOrigin,
}
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
h.logger.Error("WebSocket upgrade failed", zap.Error(err))
return
}
clientID := uuid.New().String()
wsConn := &serverless.GorillaWSConn{Conn: conn}
// Register connection
h.wsManager.Register(clientID, wsConn)
defer h.wsManager.Unregister(clientID)
h.logger.Info("WebSocket connected",
zap.String("client_id", clientID),
zap.String("function", name),
)
callerWallet := h.getWalletFromRequest(r)
// Message loop
for {
_, message, err := conn.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
h.logger.Warn("WebSocket error", zap.Error(err))
}
break
}
// Invoke function with WebSocket context
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
req := &serverless.InvokeRequest{
Namespace: namespace,
FunctionName: name,
Version: version,
Input: message,
TriggerType: serverless.TriggerTypeWebSocket,
CallerWallet: callerWallet,
WSClientID: clientID,
}
resp, err := h.invoker.Invoke(ctx, req)
cancel()
// Send response back
response := map[string]interface{}{
"request_id": resp.RequestID,
"status": resp.Status,
"duration_ms": resp.DurationMS,
}
if err != nil {
response["error"] = resp.Error
} else if len(resp.Output) > 0 {
// Try to parse output as JSON
var output interface{}
if json.Unmarshal(resp.Output, &output) == nil {
response["output"] = output
} else {
response["output"] = string(resp.Output)
}
}
respBytes, _ := json.Marshal(response)
if err := conn.WriteMessage(websocket.TextMessage, respBytes); err != nil {
break
}
}
}