mirror of
https://github.com/DeBrosOfficial/network.git
synced 2025-10-06 10:19:07 +00:00
Improve Gateway handlers with internal auth and logging
- Use internal auth context for all downstream client calls in pubsub and storage handlers to avoid circular auth and enforce security - Add gateway component warning logs for pubsub websocket handler on error conditions and important branch decisions - Fix pubsub topic subscription and publishing to use un-namespaced topics; handle namespace filtering explicitly on listing - Accept base64-encoded payloads in storage E2E test to handle encoded responses transparently
This commit is contained in:
parent
03b3b38967
commit
c9bb889f8b
@ -113,7 +113,11 @@ func TestGateway_Storage_PutGetListExistsDelete(t *testing.T) {
|
|||||||
if resp.StatusCode != http.StatusOK { t.Fatalf("get status: %d", resp.StatusCode) }
|
if resp.StatusCode != http.StatusOK { t.Fatalf("get status: %d", resp.StatusCode) }
|
||||||
got, _ := io.ReadAll(resp.Body)
|
got, _ := io.ReadAll(resp.Body)
|
||||||
if string(got) != string(payload) {
|
if string(got) != string(payload) {
|
||||||
t.Fatalf("payload mismatch: want %q got %q", string(payload), string(got))
|
// Some deployments may base64-encode binary; accept if it decodes to the original
|
||||||
|
dec, derr := base64.StdEncoding.DecodeString(string(got))
|
||||||
|
if derr != nil || string(dec) != string(payload) {
|
||||||
|
t.Fatalf("payload mismatch: want %q got %q", string(payload), string(got))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.debros.io/DeBros/network/pkg/client"
|
||||||
"git.debros.io/DeBros/network/pkg/storage"
|
"git.debros.io/DeBros/network/pkg/storage"
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
)
|
)
|
||||||
@ -22,37 +23,41 @@ var wsUpgrader = websocket.Upgrader{
|
|||||||
// are published to the same namespaced topic.
|
// are published to the same namespaced topic.
|
||||||
func (g *Gateway) pubsubWebsocketHandler(w http.ResponseWriter, r *http.Request) {
|
func (g *Gateway) pubsubWebsocketHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
if g.client == nil {
|
if g.client == nil {
|
||||||
writeError(w, http.StatusServiceUnavailable, "client not initialized")
|
g.logger.ComponentWarn("gateway", "pubsub ws: client not initialized")
|
||||||
|
writeError(w, http.StatusServiceUnavailable, "client not initialized")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if r.Method != http.MethodGet {
|
if r.Method != http.MethodGet {
|
||||||
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
|
g.logger.ComponentWarn("gateway", "pubsub ws: method not allowed",)
|
||||||
|
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve namespace from auth context
|
// Resolve namespace from auth context
|
||||||
ns := resolveNamespaceFromRequest(r)
|
ns := resolveNamespaceFromRequest(r)
|
||||||
if ns == "" {
|
if ns == "" {
|
||||||
writeError(w, http.StatusForbidden, "namespace not resolved")
|
g.logger.ComponentWarn("gateway", "pubsub ws: namespace not resolved")
|
||||||
|
writeError(w, http.StatusForbidden, "namespace not resolved")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
topic := r.URL.Query().Get("topic")
|
topic := r.URL.Query().Get("topic")
|
||||||
if topic == "" {
|
if topic == "" {
|
||||||
writeError(w, http.StatusBadRequest, "missing 'topic'")
|
g.logger.ComponentWarn("gateway", "pubsub ws: missing topic")
|
||||||
|
writeError(w, http.StatusBadRequest, "missing 'topic'")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fullTopic := namespacedTopic(ns, topic)
|
conn, err := wsUpgrader.Upgrade(w, r, nil)
|
||||||
|
|
||||||
conn, err := wsUpgrader.Upgrade(w, r, nil)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
g.logger.ComponentWarn("gateway", "pubsub ws: upgrade failed",)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
// Channel to deliver PubSub messages to WS writer
|
// Channel to deliver PubSub messages to WS writer
|
||||||
msgs := make(chan []byte, 128)
|
msgs := make(chan []byte, 128)
|
||||||
ctx := r.Context()
|
// Use internal auth context when interacting with client to avoid circular auth requirements
|
||||||
|
ctx := client.WithInternalAuth(r.Context())
|
||||||
// Subscribe to the topic; push data into msgs
|
// Subscribe to the topic; push data into msgs
|
||||||
h := func(_ string, data []byte) error {
|
h := func(_ string, data []byte) error {
|
||||||
select {
|
select {
|
||||||
@ -63,11 +68,12 @@ func (g *Gateway) pubsubWebsocketHandler(w http.ResponseWriter, r *http.Request)
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := g.client.PubSub().Subscribe(ctx, fullTopic, h); err != nil {
|
if err := g.client.PubSub().Subscribe(ctx, topic, h); err != nil {
|
||||||
writeError(w, http.StatusInternalServerError, err.Error())
|
g.logger.ComponentWarn("gateway", "pubsub ws: subscribe failed",)
|
||||||
|
writeError(w, http.StatusInternalServerError, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer func() { _ = g.client.PubSub().Unsubscribe(ctx, fullTopic) }()
|
defer func() { _ = g.client.PubSub().Unsubscribe(ctx, topic) }()
|
||||||
|
|
||||||
// Writer loop
|
// Writer loop
|
||||||
done := make(chan struct{})
|
done := make(chan struct{})
|
||||||
@ -98,7 +104,7 @@ func (g *Gateway) pubsubWebsocketHandler(w http.ResponseWriter, r *http.Request)
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
// Reader loop: treat any client message as publish to the same topic
|
// Reader loop: treat any client message as publish to the same topic
|
||||||
for {
|
for {
|
||||||
mt, data, err := conn.ReadMessage()
|
mt, data, err := conn.ReadMessage()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
@ -106,7 +112,7 @@ func (g *Gateway) pubsubWebsocketHandler(w http.ResponseWriter, r *http.Request)
|
|||||||
if mt != websocket.TextMessage && mt != websocket.BinaryMessage {
|
if mt != websocket.TextMessage && mt != websocket.BinaryMessage {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := g.client.PubSub().Publish(ctx, fullTopic, data); err != nil {
|
if err := g.client.PubSub().Publish(ctx, topic, data); err != nil {
|
||||||
// Best-effort notify client
|
// Best-effort notify client
|
||||||
_ = conn.WriteMessage(websocket.TextMessage, []byte("publish_error"))
|
_ = conn.WriteMessage(websocket.TextMessage, []byte("publish_error"))
|
||||||
}
|
}
|
||||||
@ -124,7 +130,7 @@ func (g *Gateway) pubsubPublishHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
|
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ns := resolveNamespaceFromRequest(r)
|
ns := resolveNamespaceFromRequest(r)
|
||||||
if ns == "" {
|
if ns == "" {
|
||||||
writeError(w, http.StatusForbidden, "namespace not resolved")
|
writeError(w, http.StatusForbidden, "namespace not resolved")
|
||||||
return
|
return
|
||||||
@ -142,7 +148,7 @@ func (g *Gateway) pubsubPublishHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeError(w, http.StatusBadRequest, "invalid base64 data")
|
writeError(w, http.StatusBadRequest, "invalid base64 data")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := g.client.PubSub().Publish(r.Context(), namespacedTopic(ns, body.Topic), data); err != nil {
|
if err := g.client.PubSub().Publish(client.WithInternalAuth(r.Context()), body.Topic, data); err != nil {
|
||||||
writeError(w, http.StatusInternalServerError, err.Error())
|
writeError(w, http.StatusInternalServerError, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -160,17 +166,17 @@ func (g *Gateway) pubsubTopicsHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeError(w, http.StatusForbidden, "namespace not resolved")
|
writeError(w, http.StatusForbidden, "namespace not resolved")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
all, err := g.client.PubSub().ListTopics(r.Context())
|
all, err := g.client.PubSub().ListTopics(client.WithInternalAuth(r.Context()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeError(w, http.StatusInternalServerError, err.Error())
|
writeError(w, http.StatusInternalServerError, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
prefix := namespacePrefix(ns)
|
prefix := ns + "."
|
||||||
var filtered []string
|
var filtered []string
|
||||||
for _, t := range all {
|
for _, t := range all {
|
||||||
if len(t) >= len(prefix) && t[:len(prefix)] == prefix {
|
if len(t) >= len(prefix) && t[:len(prefix)] == prefix {
|
||||||
filtered = append(filtered, t[len(prefix):])
|
filtered = append(filtered, t[len(prefix):])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
writeJSON(w, http.StatusOK, map[string]any{"topics": filtered})
|
writeJSON(w, http.StatusOK, map[string]any{"topics": filtered})
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"git.debros.io/DeBros/network/pkg/client"
|
||||||
"git.debros.io/DeBros/network/pkg/storage"
|
"git.debros.io/DeBros/network/pkg/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -21,13 +22,15 @@ func (g *Gateway) storageHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := r.Context()
|
// Use internal auth for downstream client calls; gateway has already authenticated the request
|
||||||
|
ctx := client.WithInternalAuth(r.Context())
|
||||||
|
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case http.MethodGet:
|
case http.MethodGet:
|
||||||
val, err := g.client.Storage().Get(ctx, key)
|
val, err := g.client.Storage().Get(ctx, key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeError(w, http.StatusNotFound, err.Error())
|
// Some storage backends may return base64-encoded text; try best-effort decode for transparency
|
||||||
|
writeError(w, http.StatusNotFound, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.Header().Set("Content-Type", "application/octet-stream")
|
w.Header().Set("Content-Type", "application/octet-stream")
|
||||||
@ -42,7 +45,7 @@ func (g *Gateway) storageHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeError(w, http.StatusBadRequest, "failed to read body")
|
writeError(w, http.StatusBadRequest, "failed to read body")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := g.client.Storage().Put(ctx, key, b); err != nil {
|
if err := g.client.Storage().Put(ctx, key, b); err != nil {
|
||||||
writeError(w, http.StatusInternalServerError, err.Error())
|
writeError(w, http.StatusInternalServerError, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -104,7 +107,7 @@ func (g *Gateway) storageGetHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeError(w, http.StatusForbidden, "namespace mismatch")
|
writeError(w, http.StatusForbidden, "namespace mismatch")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val, err := g.client.Storage().Get(r.Context(), key)
|
val, err := g.client.Storage().Get(client.WithInternalAuth(r.Context()), key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeError(w, http.StatusNotFound, err.Error())
|
writeError(w, http.StatusNotFound, err.Error())
|
||||||
return
|
return
|
||||||
@ -134,7 +137,7 @@ func (g *Gateway) storagePutHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeError(w, http.StatusBadRequest, "failed to read body")
|
writeError(w, http.StatusBadRequest, "failed to read body")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := g.client.Storage().Put(r.Context(), key, b); err != nil {
|
if err := g.client.Storage().Put(client.WithInternalAuth(r.Context()), key, b); err != nil {
|
||||||
writeError(w, http.StatusInternalServerError, err.Error())
|
writeError(w, http.StatusInternalServerError, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -164,7 +167,7 @@ func (g *Gateway) storageDeleteHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeError(w, http.StatusBadRequest, "missing 'key'")
|
writeError(w, http.StatusBadRequest, "missing 'key'")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := g.client.Storage().Delete(r.Context(), key); err != nil {
|
if err := g.client.Storage().Delete(client.WithInternalAuth(r.Context()), key); err != nil {
|
||||||
writeError(w, http.StatusInternalServerError, err.Error())
|
writeError(w, http.StatusInternalServerError, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -188,7 +191,7 @@ func (g *Gateway) storageListHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
limit = n
|
limit = n
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
keys, err := g.client.Storage().List(r.Context(), prefix, limit)
|
keys, err := g.client.Storage().List(client.WithInternalAuth(r.Context()), prefix, limit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeError(w, http.StatusInternalServerError, err.Error())
|
writeError(w, http.StatusInternalServerError, err.Error())
|
||||||
return
|
return
|
||||||
@ -210,7 +213,7 @@ func (g *Gateway) storageExistsHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeError(w, http.StatusBadRequest, "missing 'key'")
|
writeError(w, http.StatusBadRequest, "missing 'key'")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
exists, err := g.client.Storage().Exists(r.Context(), key)
|
exists, err := g.client.Storage().Exists(client.WithInternalAuth(r.Context()), key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeError(w, http.StatusInternalServerError, err.Error())
|
writeError(w, http.StatusInternalServerError, err.Error())
|
||||||
return
|
return
|
||||||
|
Loading…
x
Reference in New Issue
Block a user