Frame-by-frame IMQ with an outgoing filter; HTTP interceptor hook
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
2551888ed6
commit
2190230f81
6 files changed
+485
-24
No files matched your search
+89
-21
@@ -65,8 +65,11 @@ func (s *Server) serveConnect(w http.ResponseWriter, r *http.Request) {
|
||||
client = io.MultiReader(bytes.NewReader(bytes.Clone(head)), conn)
|
||||
}
|
||||
|
||||
up := &stream{s: s, id: id, name: "c2g"}
|
||||
// Frames go to the client whole, from the gateway or from the proxy answering a dropped message, so they
|
||||
// never interleave.
|
||||
toClient := &lockedWriter{w: conn}
|
||||
down := &stream{s: s, id: id, name: "g2c"}
|
||||
up := &stream{s: s, id: id, name: "c2g", filter: s.IMQFilter, reply: toClient}
|
||||
// IMQ is a session: when either side closes, it's over, so both connections close.
|
||||
var once sync.Once
|
||||
closeBoth := func() {
|
||||
@@ -85,21 +88,43 @@ func (s *Server) serveConnect(w http.ResponseWriter, r *http.Request) {
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
defer closeBoth()
|
||||
down.copy(conn, upstream)
|
||||
down.copy(toClient, upstream)
|
||||
}()
|
||||
wg.Wait()
|
||||
s.logf("#%d CONNECT %s closed after %s: %s, %s", id, target, since(start), up.summary(), down.summary())
|
||||
}
|
||||
|
||||
// stream is one direction of a CONNECT, counting bytes and IMQ frames as they pass.
|
||||
// IMQFilter sees the client's outgoing IMQ messages and can change or drop them.
|
||||
type IMQFilter interface {
|
||||
// Outgoing returns the message to send instead of message, or drop to send nothing and tell the client it
|
||||
// was sent.
|
||||
Outgoing(queue, mount string, message []byte) (out []byte, drop bool)
|
||||
}
|
||||
|
||||
// lockedWriter serializes writes, each a whole number of frames.
|
||||
type lockedWriter struct {
|
||||
mu sync.Mutex
|
||||
w io.Writer
|
||||
}
|
||||
|
||||
func (l *lockedWriter) Write(b []byte) (int, error) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
return l.w.Write(b)
|
||||
}
|
||||
|
||||
// stream is one direction of a CONNECT. It passes IMQ frames on whole, counting them (and, outgoing, filtering
|
||||
// them), or bytes as they come once the stream turns out not to be IMQ.
|
||||
type stream struct {
|
||||
s *Server
|
||||
id int64
|
||||
name string
|
||||
bytes int64
|
||||
frames int
|
||||
other bool // not IMQ framing
|
||||
reader imq.Reader
|
||||
s *Server
|
||||
id int64
|
||||
name string
|
||||
bytes int64
|
||||
frames int
|
||||
other bool // not IMQ framing
|
||||
pending []byte
|
||||
filter IMQFilter // outgoing only
|
||||
reply io.Writer // outgoing only: to the client
|
||||
}
|
||||
|
||||
func (st *stream) copy(dst io.Writer, src io.Reader) {
|
||||
@@ -107,8 +132,8 @@ func (st *stream) copy(dst io.Writer, src io.Reader) {
|
||||
for {
|
||||
n, err := src.Read(buf)
|
||||
if n > 0 {
|
||||
st.observe(buf[:n])
|
||||
if _, werr := dst.Write(buf[:n]); werr != nil {
|
||||
st.bytes += int64(n)
|
||||
if werr := st.pass(dst, buf[:n]); werr != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -118,22 +143,65 @@ func (st *stream) copy(dst io.Writer, src io.Reader) {
|
||||
}
|
||||
}
|
||||
|
||||
func (st *stream) observe(data []byte) {
|
||||
st.bytes += int64(len(data))
|
||||
// pass writes on the frames data completes.
|
||||
func (st *stream) pass(dst io.Writer, data []byte) error {
|
||||
if st.other {
|
||||
return
|
||||
_, err := dst.Write(data)
|
||||
return err
|
||||
}
|
||||
frames, err := st.reader.Write(data)
|
||||
st.frames += len(frames)
|
||||
if st.s.Trace {
|
||||
for _, f := range frames {
|
||||
st.pending = append(st.pending, data...)
|
||||
var out []byte
|
||||
for {
|
||||
f, n, err := imq.Next(st.pending)
|
||||
if err != nil {
|
||||
st.other = true
|
||||
st.s.logf("#%d %s isn't IMQ framing; passing it through", st.id, st.name)
|
||||
out = append(out, st.pending...)
|
||||
st.pending = nil
|
||||
break
|
||||
}
|
||||
if n == 0 {
|
||||
break
|
||||
}
|
||||
frame := st.pending[:n]
|
||||
st.pending = st.pending[n:]
|
||||
st.frames++
|
||||
if st.s.Trace {
|
||||
st.s.logf("#%d %s %s %dB", st.id, st.name, imq.TypeName(f.Type), f.Size)
|
||||
}
|
||||
out = append(out, st.filtered(f, frame)...)
|
||||
}
|
||||
if len(st.pending) == 0 {
|
||||
st.pending = nil
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil
|
||||
}
|
||||
_, err := dst.Write(out)
|
||||
return err
|
||||
}
|
||||
|
||||
// filtered is the frame to send on: as is, changed by the filter, or none (answered here).
|
||||
func (st *stream) filtered(f imq.Frame, frame []byte) []byte {
|
||||
if st.filter == nil || f.Type != imq.C2gSendMessage {
|
||||
return frame
|
||||
}
|
||||
m, err := imq.ParseSendMessage(f.Data)
|
||||
if err != nil {
|
||||
st.other = true
|
||||
st.s.logf("#%d %s isn't IMQ framing; passing it through", st.id, st.name)
|
||||
return frame
|
||||
}
|
||||
out, drop := st.filter.Outgoing(m.Queue, m.Mount, m.Message)
|
||||
if drop {
|
||||
if st.reply != nil {
|
||||
st.reply.Write(imq.EncodeFrame(imq.G2cResult, imq.Result(m.OpID, 0)))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if bytes.Equal(out, m.Message) {
|
||||
return frame
|
||||
}
|
||||
m.Message = out
|
||||
return imq.EncodeFrame(imq.C2gSendMessage, m.Marshal())
|
||||
}
|
||||
|
||||
func (st *stream) summary() string {
|
||||
|
||||
Reference in new issue
Block a user