feat(push-gateway): config 扩展 + kafka consumer + ws handler + nextstep 文档
This commit is contained in:
@@ -6,8 +6,12 @@
|
||||
// - WebSocket /ws: JWT RS256 validated via shared-go/jwks (caching JWKS from
|
||||
// iam /.well-known/jwks.json, refreshed every 5 minutes). DevMode accepts
|
||||
// the literal "dev-token" returning a synthetic dev-user subject.
|
||||
// - /internal/*: X-Internal-Token header matched against INTERNAL_API_TOKEN
|
||||
// (ARB-015 §17.3 / president §7.2). DevMode skips the check.
|
||||
// - /internal/*: X-Internal-Key header matched against PUSH_INTERNAL_TOKEN
|
||||
// (ARB-013 alignment with msg). X-Internal-Token is accepted as a
|
||||
// backward-compat alias. DevMode skips the check.
|
||||
//
|
||||
// Request body field naming: msg sends userId (camelCase); legacy callers may
|
||||
// still send user_id (snake_case). Both are accepted by /internal/push.
|
||||
//
|
||||
// Heartbeat (RFC 6455 control frames, not text messages):
|
||||
// - Client sends Ping every 30s; gorilla/websocket auto-replies with Pong.
|
||||
@@ -40,8 +44,12 @@ import (
|
||||
)
|
||||
|
||||
// internalTokenHeader is the canonical header for /internal/* authentication
|
||||
// (ARB-015 §17.3 / president §7.2). Was X-Internal-Key in pre-P5 builds.
|
||||
const internalTokenHeader = "X-Internal-Token"
|
||||
// (ARB-013 alignment with msg). legacyInternalTokenHeader is accepted as a
|
||||
// backward-compat alias for existing callers.
|
||||
const (
|
||||
internalTokenHeader = "X-Internal-Key"
|
||||
legacyInternalTokenHeader = "X-Internal-Token"
|
||||
)
|
||||
|
||||
// readBufferSize / writeBufferSize tune gorilla/websocket's internal buffers.
|
||||
const (
|
||||
@@ -244,16 +252,20 @@ func (h *Handler) readerLoop(c *hub.Connection) {
|
||||
|
||||
// PushHandler implements POST /internal/push: directed push to a single user.
|
||||
// Response carries delivered + online so msg (ai10) can decide offline fallback.
|
||||
//
|
||||
// Request body accepts both camelCase (userId, msg's preferred format) and
|
||||
// snake_case (user_id, legacy format) field names for the user identifier.
|
||||
func (h *Handler) PushHandler(c *gin.Context) {
|
||||
if !h.checkInternalToken(c) {
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
UserID string `json:"user_id" binding:"required"`
|
||||
Event string `json:"event" binding:"required"`
|
||||
Data map[string]any `json:"data"`
|
||||
TTL *int `json:"ttl,omitempty"`
|
||||
UserID string `json:"userId"` // camelCase (msg canonical, ARB-013)
|
||||
UserIDLeg string `json:"user_id"` // snake_case (legacy)
|
||||
Event string `json:"event" binding:"required"`
|
||||
Data map[string]any `json:"data"`
|
||||
TTL *int `json:"ttl,omitempty"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, pushResponse{
|
||||
@@ -262,6 +274,17 @@ func (h *Handler) PushHandler(c *gin.Context) {
|
||||
})
|
||||
return
|
||||
}
|
||||
// Prefer userId (camelCase); fall back to user_id (snake_case, legacy).
|
||||
if req.UserID == "" {
|
||||
req.UserID = req.UserIDLeg
|
||||
}
|
||||
if req.UserID == "" {
|
||||
c.AbortWithStatusJSON(http.StatusBadRequest, pushResponse{
|
||||
Success: false,
|
||||
Error: &errBody{Code: "PUSH_INVALID_REQUEST", Message: "userId (or user_id) is required"},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
message, err := buildMessage(req.Event, req.Data)
|
||||
if err != nil {
|
||||
@@ -399,8 +422,10 @@ func (h *Handler) OnlineHandler(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"online": false, "instances": []string{}})
|
||||
}
|
||||
|
||||
// checkInternalToken validates the X-Internal-Token header. Returns false when
|
||||
// the request has been aborted (caller should return immediately).
|
||||
// checkInternalToken validates the internal token header. The canonical header
|
||||
// is X-Internal-Key (ARB-013 alignment with msg); X-Internal-Token is accepted
|
||||
// as a backward-compat alias. Returns false when the request has been aborted
|
||||
// (caller should return immediately).
|
||||
func (h *Handler) checkInternalToken(c *gin.Context) bool {
|
||||
if h.cfg.DevMode {
|
||||
return true
|
||||
@@ -412,7 +437,11 @@ func (h *Handler) checkInternalToken(c *gin.Context) bool {
|
||||
})
|
||||
return false
|
||||
}
|
||||
if c.GetHeader(internalTokenHeader) != h.cfg.InternalAPIToken {
|
||||
token := c.GetHeader(internalTokenHeader)
|
||||
if token == "" {
|
||||
token = c.GetHeader(legacyInternalTokenHeader)
|
||||
}
|
||||
if token != h.cfg.InternalAPIToken {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, pushResponse{
|
||||
Success: false,
|
||||
Error: &errBody{Code: "PUSH_UNAUTHORIZED", Message: "invalid or missing internal token"},
|
||||
|
||||
Reference in New Issue
Block a user