mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-09-03 08:18:42 +00:00
186 lines
6.4 KiB
Go
186 lines
6.4 KiB
Go
package http
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"log/slog"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/nextlevelbuilder/goclaw/internal/i18n"
|
|
"github.com/nextlevelbuilder/goclaw/internal/permissions"
|
|
"github.com/nextlevelbuilder/goclaw/internal/store"
|
|
)
|
|
|
|
const (
|
|
maxBrowserCookieSyncBodyBytes = 1 << 20
|
|
maxBrowserCookieSyncItems = 200
|
|
maxBrowserCookieValueBytes = 16 << 10
|
|
)
|
|
|
|
var (
|
|
errBrowserCookieInvalidURL = errors.New("invalid cookie url")
|
|
errBrowserCookieValueTooLarge = errors.New("cookie value too large")
|
|
errBrowserCookieTooManyCookies = errors.New("too many cookies")
|
|
)
|
|
|
|
// BrowserCookiesHandler stores selected browser cookies for server-side browser sessions.
|
|
type BrowserCookiesHandler struct {
|
|
cookies store.BrowserCookieStore
|
|
}
|
|
|
|
func NewBrowserCookiesHandler(cookies store.BrowserCookieStore) *BrowserCookiesHandler {
|
|
return &BrowserCookiesHandler{cookies: cookies}
|
|
}
|
|
|
|
func (h *BrowserCookiesHandler) RegisterRoutes(mux *http.ServeMux) {
|
|
mux.HandleFunc("POST /v1/browser/cookies/sync", requireAuth(permissions.RoleOperator, h.handleSync))
|
|
mux.HandleFunc("GET /v1/browser/cookies", requireAuth(permissions.RoleOperator, h.handleList))
|
|
mux.HandleFunc("DELETE /v1/browser/cookies", requireAuth(permissions.RoleOperator, h.handleDelete))
|
|
}
|
|
|
|
func (h *BrowserCookiesHandler) handleSync(w http.ResponseWriter, r *http.Request) {
|
|
locale := store.LocaleFromContext(r.Context())
|
|
var req browserCookieSyncRequest
|
|
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, maxBrowserCookieSyncBodyBytes)).Decode(&req); err != nil {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgInvalidJSON)})
|
|
return
|
|
}
|
|
|
|
agentID := firstBrowserCookieNonEmpty(req.AgentID, req.Agent)
|
|
scope, ok := h.scopeFromRequest(w, r, agentID)
|
|
if !ok {
|
|
return
|
|
}
|
|
if len(req.Cookies) == 0 {
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": i18n.T(locale, i18n.MsgRequired, "cookies")})
|
|
return
|
|
}
|
|
if len(req.Cookies) > maxBrowserCookieSyncItems {
|
|
h.writeValidationError(w, locale, errBrowserCookieTooManyCookies)
|
|
return
|
|
}
|
|
|
|
source := strings.TrimSpace(req.Source)
|
|
if source == "" {
|
|
source = "chrome-extension"
|
|
}
|
|
cookies := make([]store.BrowserCookie, 0, len(req.Cookies))
|
|
for _, item := range req.Cookies {
|
|
c, err := item.toStoreCookie(source)
|
|
if err != nil {
|
|
h.writeValidationError(w, locale, err)
|
|
return
|
|
}
|
|
cookies = append(cookies, c)
|
|
}
|
|
|
|
count, err := h.cookies.Upsert(r.Context(), scope, cookies)
|
|
if err != nil {
|
|
h.writeStoreError(w, locale, "browser_cookie_sync.upsert", err)
|
|
return
|
|
}
|
|
slog.Info("browser_cookie_sync.synced",
|
|
"tenant_id", scope.TenantID,
|
|
"user_id", scope.UserID,
|
|
"agent_id", scope.AgentID,
|
|
"count", count,
|
|
"source", source,
|
|
)
|
|
writeJSON(w, http.StatusOK, map[string]any{"synced": count})
|
|
}
|
|
|
|
func (h *BrowserCookiesHandler) handleList(w http.ResponseWriter, r *http.Request) {
|
|
locale := store.LocaleFromContext(r.Context())
|
|
scope, ok := h.scopeFromRequest(w, r, r.URL.Query().Get("agent_id"))
|
|
if !ok {
|
|
return
|
|
}
|
|
cookies, err := h.cookies.List(r.Context(), scope, browserCookieFilterFromQuery(r))
|
|
if err != nil {
|
|
h.writeStoreError(w, locale, "browser_cookie_sync.list", err)
|
|
return
|
|
}
|
|
items := make([]browserCookieMetadata, 0, len(cookies))
|
|
for _, c := range cookies {
|
|
items = append(items, browserCookieMetadata{
|
|
Domain: c.Domain,
|
|
Name: c.Name,
|
|
Path: c.Path,
|
|
Secure: c.Secure,
|
|
HTTPOnly: c.HTTPOnly,
|
|
SameSite: c.SameSite,
|
|
ExpiresAt: c.ExpiresAt,
|
|
Source: c.Source,
|
|
UpdatedAt: c.UpdatedAt,
|
|
})
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"items": items})
|
|
}
|
|
|
|
func (h *BrowserCookiesHandler) handleDelete(w http.ResponseWriter, r *http.Request) {
|
|
locale := store.LocaleFromContext(r.Context())
|
|
scope, ok := h.scopeFromRequest(w, r, r.URL.Query().Get("agent_id"))
|
|
if !ok {
|
|
return
|
|
}
|
|
deleted, err := h.cookies.Delete(r.Context(), scope, browserCookieFilterFromQuery(r))
|
|
if err != nil {
|
|
h.writeStoreError(w, locale, "browser_cookie_sync.delete", err)
|
|
return
|
|
}
|
|
slog.Info("browser_cookie_sync.deleted",
|
|
"tenant_id", scope.TenantID,
|
|
"user_id", scope.UserID,
|
|
"agent_id", scope.AgentID,
|
|
"count", deleted,
|
|
)
|
|
writeJSON(w, http.StatusOK, map[string]any{"deleted": deleted})
|
|
}
|
|
|
|
func (h *BrowserCookiesHandler) scopeFromRequest(w http.ResponseWriter, r *http.Request, agentID string) (store.BrowserCookieScope, bool) {
|
|
locale := store.LocaleFromContext(r.Context())
|
|
scope := store.BrowserCookieScopeFromContext(r.Context(), agentID)
|
|
if err := scope.Validate(); err != nil {
|
|
slog.Warn("security.browser_cookie_sync.scope_denied", "error", err, "path", r.URL.Path)
|
|
h.writeValidationError(w, locale, err)
|
|
return store.BrowserCookieScope{}, false
|
|
}
|
|
return scope, true
|
|
}
|
|
|
|
func (h *BrowserCookiesHandler) writeValidationError(w http.ResponseWriter, locale string, err error) {
|
|
msg := i18n.T(locale, i18n.MsgInvalidRequest, "browser cookies")
|
|
switch {
|
|
case errors.Is(err, store.ErrBrowserCookieTenantRequired):
|
|
msg = i18n.T(locale, i18n.MsgRequired, "tenant_id")
|
|
case errors.Is(err, store.ErrBrowserCookieUserRequired):
|
|
msg = i18n.T(locale, i18n.MsgRequired, "user_id")
|
|
case errors.Is(err, store.ErrBrowserCookieAgentRequired):
|
|
msg = i18n.T(locale, i18n.MsgRequired, "agent_id")
|
|
case errors.Is(err, store.ErrBrowserCookieDomainRequired):
|
|
msg = i18n.T(locale, i18n.MsgRequired, "domain")
|
|
case errors.Is(err, store.ErrBrowserCookieNameRequired):
|
|
msg = i18n.T(locale, i18n.MsgRequired, "name")
|
|
case errors.Is(err, store.ErrBrowserCookiePathRequired):
|
|
msg = i18n.T(locale, i18n.MsgRequired, "path")
|
|
case errors.Is(err, errBrowserCookieInvalidURL):
|
|
msg = i18n.T(locale, i18n.MsgInvalidCookieURL)
|
|
case errors.Is(err, errBrowserCookieValueTooLarge):
|
|
msg = i18n.T(locale, i18n.MsgBrowserCookieValueTooLarge)
|
|
case errors.Is(err, errBrowserCookieTooManyCookies):
|
|
msg = i18n.T(locale, i18n.MsgBrowserCookieTooMany)
|
|
}
|
|
writeJSON(w, http.StatusBadRequest, map[string]string{"error": msg})
|
|
}
|
|
|
|
func (h *BrowserCookiesHandler) writeStoreError(w http.ResponseWriter, locale, op string, err error) {
|
|
if errors.Is(err, store.ErrBrowserCookieEncryptionRequired) {
|
|
slog.Warn("security.browser_cookie_sync.encryption_required", "op", op)
|
|
writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": i18n.T(locale, i18n.MsgBrowserCookieEncryptionRequired)})
|
|
return
|
|
}
|
|
slog.Error(op, "error", err)
|
|
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": i18n.T(locale, i18n.MsgInternalError, "browser cookies")})
|
|
}
|