feat: 接线 serve 真实 auth/注册/管理/MQTT 与消息循环
This commit is contained in:
+241
-74
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -14,7 +15,18 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.asio.asia/nixevol/NixMsg/internal/admin"
|
||||||
|
"git.asio.asia/nixevol/NixMsg/internal/app/group"
|
||||||
|
"git.asio.asia/nixevol/NixMsg/internal/app/identity"
|
||||||
|
"git.asio.asia/nixevol/NixMsg/internal/app/message"
|
||||||
|
"git.asio.asia/nixevol/NixMsg/internal/app/port"
|
||||||
|
"git.asio.asia/nixevol/NixMsg/internal/app/presence"
|
||||||
|
"git.asio.asia/nixevol/NixMsg/internal/auth"
|
||||||
|
"git.asio.asia/nixevol/NixMsg/internal/broker"
|
||||||
"git.asio.asia/nixevol/NixMsg/internal/config"
|
"git.asio.asia/nixevol/NixMsg/internal/config"
|
||||||
|
"git.asio.asia/nixevol/NixMsg/internal/httpx"
|
||||||
|
"git.asio.asia/nixevol/NixMsg/internal/listener"
|
||||||
|
"git.asio.asia/nixevol/NixMsg/internal/metrics"
|
||||||
"git.asio.asia/nixevol/NixMsg/internal/store"
|
"git.asio.asia/nixevol/NixMsg/internal/store"
|
||||||
"git.asio.asia/nixevol/NixMsg/web"
|
"git.asio.asia/nixevol/NixMsg/web"
|
||||||
)
|
)
|
||||||
@@ -25,20 +37,16 @@ func cmdServe(_ []string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// P-WIRE-BEGIN
|
|
||||||
if err := cfg.Validate(); err != nil {
|
if err := cfg.Validate(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
setupJSONLogger(cfg.Log)
|
setupJSONLogger(cfg.Log)
|
||||||
// P-WIRE-END
|
|
||||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||||
defer stop()
|
defer stop()
|
||||||
return runServe(ctx, cfg)
|
return runServe(ctx, cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func runServe(ctx context.Context, cfg config.Config) error {
|
func runServe(ctx context.Context, cfg config.Config) error {
|
||||||
deps := wire()
|
|
||||||
|
|
||||||
if err := os.MkdirAll(cfg.DataDir, 0o755); err != nil {
|
if err := os.MkdirAll(cfg.DataDir, 0o755); err != nil {
|
||||||
return fmt.Errorf("mkdir data_dir: %w", err)
|
return fmt.Errorf("mkdir data_dir: %w", err)
|
||||||
}
|
}
|
||||||
@@ -49,7 +57,6 @@ func runServe(ctx context.Context, cfg config.Config) error {
|
|||||||
}
|
}
|
||||||
defer func() { _ = db.Close() }()
|
defer func() { _ = db.Close() }()
|
||||||
|
|
||||||
// P-WIRE-BEGIN
|
|
||||||
ok, err := store.HasAdminPassword(ctx, db.Write)
|
ok, err := store.HasAdminPassword(ctx, db.Write)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -57,87 +64,250 @@ func runServe(ctx context.Context, cfg config.Config) error {
|
|||||||
if !ok {
|
if !ok {
|
||||||
return errors.New("admin password not initialized; run: nixmsg admin init")
|
return errors.New("admin password not initialized; run: nixmsg admin init")
|
||||||
}
|
}
|
||||||
// P-WIRE-END
|
|
||||||
|
|
||||||
// 启动恢复入口已挂上(假实现为空操作);M 线替换 message.Service 后生效。
|
hashPool := auth.NewPool()
|
||||||
if recoverErr := deps.Messages.RecoverOnStart(ctx); recoverErr != nil {
|
sessionTokens := auth.NewSessionTokens()
|
||||||
|
apiTokens := auth.NewAPITokens()
|
||||||
|
loginLocks := auth.NewLoginLocks()
|
||||||
|
memConns := message.NewMemoryConns()
|
||||||
|
msgLim := message.LimitsFromFullConfig(cfg)
|
||||||
|
|
||||||
|
login := broker.NewLogin(broker.LoginOptions{
|
||||||
|
DB: db,
|
||||||
|
Pool: hashPool,
|
||||||
|
Tokens: sessionTokens,
|
||||||
|
Locks: loginLocks,
|
||||||
|
IdleDays: cfg.SessionIdleDays,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 先用空下行建 message,拿到 App 指针;broker 建好后再 WithDownlink 重建并回填 uplink.msg。
|
||||||
|
msgApp := message.New(db, msgLim, hashPool,
|
||||||
|
message.WithLocks(loginLocks),
|
||||||
|
message.WithConnRegistry(memConns),
|
||||||
|
)
|
||||||
|
uplink := &appUplink{
|
||||||
|
msg: msgApp,
|
||||||
|
conns: memConns,
|
||||||
|
next: port.StubUplinkHandler{},
|
||||||
|
log: slog.Default(),
|
||||||
|
}
|
||||||
|
sess := broker.NewSession(broker.SessionOptions{
|
||||||
|
Login: login,
|
||||||
|
Inner: uplink,
|
||||||
|
Limits: broker.HelloLimits{
|
||||||
|
MaxBodyBytes: cfg.Limits.MaxBodyBytes,
|
||||||
|
MaxMetaBytes: cfg.Limits.MaxMetaBytes,
|
||||||
|
MaxFrameBytes: cfg.Limits.MaxFrameBytes,
|
||||||
|
MaxTTLSeconds: int64(cfg.Limits.MaxTTLSeconds),
|
||||||
|
MaxScheduleSeconds: int64(cfg.Limits.MaxScheduleSeconds),
|
||||||
|
AckTimeoutSeconds: int64(cfg.Limits.AckTimeoutSeconds),
|
||||||
|
ServerVersion: Version,
|
||||||
|
},
|
||||||
|
Logger: slog.Default(),
|
||||||
|
})
|
||||||
|
|
||||||
|
brk, err := broker.New(broker.Options{
|
||||||
|
Authenticator: login,
|
||||||
|
Uplink: sess,
|
||||||
|
Logger: slog.Default(),
|
||||||
|
OnPublishDropped: func(dropCtx context.Context, endpointID string, connID port.ConnID, payload []byte) {
|
||||||
|
if dropErr := msgApp.OnPublishDropped(dropCtx, endpointID, connID, payload); dropErr != nil {
|
||||||
|
slog.Error("on publish dropped", "endpoint", endpointID, "err", dropErr)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("broker: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = brk.Close() }()
|
||||||
|
sess.Attach(brk)
|
||||||
|
|
||||||
|
msgApp = message.New(db, msgLim, hashPool,
|
||||||
|
message.WithLocks(loginLocks),
|
||||||
|
message.WithConnRegistry(memConns),
|
||||||
|
message.WithDownlink(brk),
|
||||||
|
)
|
||||||
|
uplink.msg = msgApp
|
||||||
|
|
||||||
|
presApp := presence.New(presence.Config{
|
||||||
|
DB: db,
|
||||||
|
Downlink: brk,
|
||||||
|
Conns: &presenceConnTable{conns: memConns},
|
||||||
|
})
|
||||||
|
sess.SetPresence(presApp)
|
||||||
|
|
||||||
|
idApp := identity.New(identity.Config{
|
||||||
|
DB: db,
|
||||||
|
Hash: hashPool,
|
||||||
|
Locks: loginLocks,
|
||||||
|
Sessions: sessionTokens,
|
||||||
|
MaxScheduleSeconds: int64(cfg.Limits.MaxScheduleSeconds),
|
||||||
|
Logger: slog.Default(),
|
||||||
|
ConnControl: brk,
|
||||||
|
})
|
||||||
|
_ = group.New(group.Config{
|
||||||
|
DB: db,
|
||||||
|
Talk: idApp,
|
||||||
|
Online: presApp,
|
||||||
|
Downlink: brk,
|
||||||
|
MaxGroupMembers: cfg.Limits.MaxGroupMembers,
|
||||||
|
})
|
||||||
|
|
||||||
|
if recoverErr := msgApp.RecoverOnStart(ctx); recoverErr != nil {
|
||||||
return fmt.Errorf("message recover: %w", recoverErr)
|
return fmt.Errorf("message recover: %w", recoverErr)
|
||||||
}
|
}
|
||||||
// 其余 deps 供后续 admin / broker / httpx 接线;此处显式引用避免未使用告警。
|
|
||||||
_ = deps.Identity
|
|
||||||
_ = deps.Groups
|
|
||||||
_ = deps.Presence
|
|
||||||
_ = deps.Downlink
|
|
||||||
_ = deps.Conns
|
|
||||||
_ = deps.Uplink
|
|
||||||
_ = deps.HashPool
|
|
||||||
_ = deps.SessionTokens
|
|
||||||
_ = deps.APITokens
|
|
||||||
_ = deps.LoginLocks
|
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
trustedNets := httpx.ParseCIDRs(cfg.TrustedProxies)
|
||||||
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, _ *http.Request) {
|
adminHandler := admin.New(admin.Deps{
|
||||||
w.WriteHeader(http.StatusOK)
|
DB: db,
|
||||||
_, _ = w.Write([]byte("ok"))
|
Hash: hashPool,
|
||||||
|
Tokens: apiTokens,
|
||||||
|
Locks: loginLocks,
|
||||||
|
Logger: slog.Default(),
|
||||||
|
TrustedProxies: trustedNets,
|
||||||
|
KickEndpoint: func(kickCtx context.Context, endpointID string) (bool, error) {
|
||||||
|
if _, found := brk.ConnInfoOf(endpointID); !found {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
if kickErr := sess.Kick(kickCtx, endpointID); kickErr != nil {
|
||||||
|
return false, kickErr
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
},
|
||||||
})
|
})
|
||||||
// P-WIRE-BEGIN
|
|
||||||
mux.HandleFunc("GET /readyz", func(w http.ResponseWriter, r *http.Request) {
|
metricsReg := metrics.New()
|
||||||
if readyErr := db.Ready(r.Context()); readyErr != nil {
|
buildHandlers := func(proxies *listener.ProxySet) listener.Handlers {
|
||||||
slog.Error("readyz failed", "err", readyErr)
|
return listener.Handlers{
|
||||||
http.Error(w, "not ready", http.StatusServiceUnavailable)
|
MQTT: brk.WSHandler(proxies),
|
||||||
return
|
ClientAPI: idApp.Handler(),
|
||||||
|
AdminAPI: adminHandler,
|
||||||
|
Metrics: metricsReg.Handler(),
|
||||||
|
MetricsToken: cfg.Metrics.Token,
|
||||||
|
Static: staticFileHandler(),
|
||||||
|
Healthz: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_, _ = w.Write([]byte("ok"))
|
||||||
|
}),
|
||||||
|
Readyz: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if readyErr := db.Ready(r.Context()); readyErr != nil {
|
||||||
|
slog.Error("readyz failed", "err", readyErr)
|
||||||
|
http.Error(w, "not ready", http.StatusServiceUnavailable)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_, _ = w.Write([]byte("ok"))
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
_, _ = w.Write([]byte("ok"))
|
|
||||||
})
|
|
||||||
// P-WIRE-END
|
|
||||||
// 确保前端资源被链接进二进制;完整静态托管由后续任务完善。
|
|
||||||
_ = web.Dist()
|
|
||||||
|
|
||||||
ln, err := net.Listen("tcp", cfg.Listen)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("listen %s: %w", cfg.Listen, err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := writeListenAddr(cfg.DataDir, ln.Addr().String()); err != nil {
|
handlers := buildHandlers(nil)
|
||||||
_ = ln.Close()
|
shared := cfg.AdminListen == ""
|
||||||
|
var clientHandler, adminHTTP http.Handler
|
||||||
|
if shared {
|
||||||
|
clientHandler = listener.NewMux(listener.RoleShared, handlers)
|
||||||
|
} else {
|
||||||
|
clientHandler = listener.NewMux(listener.RoleClient, handlers)
|
||||||
|
adminHTTP = listener.NewMux(listener.RoleAdmin, handlers)
|
||||||
|
}
|
||||||
|
|
||||||
|
lnOpts := listener.Options{
|
||||||
|
Listen: cfg.Listen,
|
||||||
|
AdminListen: cfg.AdminListen,
|
||||||
|
DataDir: cfg.DataDir,
|
||||||
|
CertFile: cfg.TLS.CertFile,
|
||||||
|
KeyFile: cfg.TLS.KeyFile,
|
||||||
|
AllowPlaintext: cfg.TLS.AllowPlaintext,
|
||||||
|
TrustedProxies: cfg.TrustedProxies,
|
||||||
|
ClientHandler: clientHandler,
|
||||||
|
AdminHandler: adminHTTP,
|
||||||
|
OnMQTT: func(conn net.Conn) {
|
||||||
|
_ = brk.AttachTCP(conn)
|
||||||
|
},
|
||||||
|
Logger: slog.Default(),
|
||||||
|
}
|
||||||
|
lnSrv, err := listener.New(lnOpts)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("listener: %w", err)
|
||||||
|
}
|
||||||
|
handlers = buildHandlers(lnSrv.Proxies())
|
||||||
|
if shared {
|
||||||
|
lnOpts.ClientHandler = listener.NewMux(listener.RoleShared, handlers)
|
||||||
|
} else {
|
||||||
|
lnOpts.ClientHandler = listener.NewMux(listener.RoleClient, handlers)
|
||||||
|
lnOpts.AdminHandler = listener.NewMux(listener.RoleAdmin, handlers)
|
||||||
|
}
|
||||||
|
lnSrv, err = listener.New(lnOpts)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("listener: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := lnSrv.Start(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
defer func() { _ = lnSrv.Close() }()
|
||||||
|
|
||||||
srv := &http.Server{
|
if err := writeListenAddr(cfg.DataDir, lnSrv.ListenAddr()); err != nil {
|
||||||
Handler: mux,
|
return err
|
||||||
ReadHeaderTimeout: 10 * time.Second,
|
}
|
||||||
|
if cfg.AdminListen != "" && lnSrv.AdminAddr() != "" {
|
||||||
|
if err := os.WriteFile(filepath.Join(cfg.DataDir, "admin.addr"), []byte(lnSrv.AdminAddr()+"\n"), 0o644); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
errCh := make(chan error, 1)
|
loopCtx, loopCancel := context.WithCancel(ctx)
|
||||||
go func() {
|
defer loopCancel()
|
||||||
errCh <- srv.Serve(ln)
|
go messageLoops(loopCtx, msgApp, memConns)
|
||||||
}()
|
|
||||||
|
|
||||||
select {
|
<-ctx.Done()
|
||||||
case <-ctx.Done():
|
loopCancel()
|
||||||
// P-WIRE-BEGIN
|
_ = lnSrv.Close()
|
||||||
// 先停止接受新连接,再等写队列最多 10 秒,然后断开并退出。
|
drainCtx, drainCancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
shutdownCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
defer drainCancel()
|
||||||
defer cancel()
|
if drainErr := db.Queue.Drain(drainCtx); drainErr != nil && !errors.Is(drainErr, context.DeadlineExceeded) {
|
||||||
_ = srv.Shutdown(shutdownCtx)
|
slog.Error("write queue drain", "err", drainErr)
|
||||||
drainCtx, drainCancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
||||||
defer drainCancel()
|
|
||||||
if drainErr := db.Queue.Drain(drainCtx); drainErr != nil && !errors.Is(drainErr, context.DeadlineExceeded) {
|
|
||||||
slog.Error("write queue drain", "err", drainErr)
|
|
||||||
}
|
|
||||||
// P-WIRE-END
|
|
||||||
serveErr := <-errCh
|
|
||||||
if serveErr != nil && !errors.Is(serveErr, http.ErrServerClosed) {
|
|
||||||
return serveErr
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
case serveErr := <-errCh:
|
|
||||||
if errors.Is(serveErr, http.ErrServerClosed) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return serveErr
|
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func messageLoops(ctx context.Context, msgApp *message.App, conns *message.MemoryConns) {
|
||||||
|
t := time.NewTicker(time.Second)
|
||||||
|
defer t.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-t.C:
|
||||||
|
nowMs := time.Now().UnixMilli()
|
||||||
|
if _, err := msgApp.DispatchDue(ctx, nowMs, 100); err != nil {
|
||||||
|
slog.Error("dispatch due", "err", err)
|
||||||
|
}
|
||||||
|
for ep, live := range conns.Snapshot() {
|
||||||
|
if err := msgApp.PushPending(ctx, ep, live.ConnID); err != nil {
|
||||||
|
slog.Debug("push pending", "endpoint", ep, "err", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := msgApp.CleanupOnce(ctx, nowMs); err != nil {
|
||||||
|
slog.Error("cleanup once", "err", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func staticFileHandler() http.Handler {
|
||||||
|
root := web.Dist()
|
||||||
|
for _, prefix := range []string{"dist", "stub"} {
|
||||||
|
sub, err := fs.Sub(root, prefix)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if f, err := sub.Open("index.html"); err == nil {
|
||||||
|
_ = f.Close()
|
||||||
|
return http.FileServer(http.FS(sub))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return http.FileServer(http.FS(root))
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeListenAddr(dataDir, addr string) error {
|
func writeListenAddr(dataDir, addr string) error {
|
||||||
@@ -145,7 +315,6 @@ func writeListenAddr(dataDir, addr string) error {
|
|||||||
return os.WriteFile(path, []byte(addr+"\n"), 0o644)
|
return os.WriteFile(path, []byte(addr+"\n"), 0o644)
|
||||||
}
|
}
|
||||||
|
|
||||||
// P-WIRE-BEGIN
|
|
||||||
func setupJSONLogger(cfg config.LogConfig) {
|
func setupJSONLogger(cfg config.LogConfig) {
|
||||||
level := slog.LevelInfo
|
level := slog.LevelInfo
|
||||||
switch strings.ToLower(strings.TrimSpace(cfg.Level)) {
|
switch strings.ToLower(strings.TrimSpace(cfg.Level)) {
|
||||||
@@ -159,5 +328,3 @@ func setupJSONLogger(cfg config.LogConfig) {
|
|||||||
h := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: level})
|
h := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: level})
|
||||||
slog.SetDefault(slog.New(h))
|
slog.SetDefault(slog.New(h))
|
||||||
}
|
}
|
||||||
|
|
||||||
// P-WIRE-END
|
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
|
"git.asio.asia/nixevol/NixMsg/internal/app/message"
|
||||||
|
"git.asio.asia/nixevol/NixMsg/internal/app/port"
|
||||||
|
)
|
||||||
|
|
||||||
|
// appUplink 把 broker 生命周期接到消息连接表与投递推送。
|
||||||
|
// 其余业务上行帧暂转交 next(可为空 Stub);完整 HandleUplink 分发留后续波次。
|
||||||
|
type appUplink struct {
|
||||||
|
msg *message.App
|
||||||
|
conns *message.MemoryConns
|
||||||
|
next port.UplinkHandler
|
||||||
|
log *slog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *appUplink) OnSessionEstablished(ctx context.Context, conn port.ConnInfo) error {
|
||||||
|
u.conns.Set(conn.EndpointID, message.LiveConn{
|
||||||
|
ConnID: conn.ConnID,
|
||||||
|
MaxPacketSize: conn.MaxPacketSize,
|
||||||
|
})
|
||||||
|
if u.next != nil {
|
||||||
|
return u.next.OnSessionEstablished(ctx, conn)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *appUplink) OnHandshakeComplete(ctx context.Context, hs port.HandshakeInfo) error {
|
||||||
|
live := message.LiveConn{
|
||||||
|
ConnID: hs.ConnID,
|
||||||
|
MaxReceiveBytes: hs.MaxReceiveBytes,
|
||||||
|
MaxPacketSize: hs.MaxPacketSize,
|
||||||
|
}
|
||||||
|
u.conns.Set(hs.EndpointID, live)
|
||||||
|
if err := u.msg.OnHandshakeComplete(ctx, hs.EndpointID, live); err != nil {
|
||||||
|
u.log.Error("message handshake", "endpoint", hs.EndpointID, "err", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if u.next != nil {
|
||||||
|
return u.next.OnHandshakeComplete(ctx, hs)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *appUplink) OnDisconnect(ctx context.Context, conn port.ConnInfo, reason port.DisconnectReason) {
|
||||||
|
live, ok := u.conns.Current(conn.EndpointID)
|
||||||
|
isCurrent := ok && live.ConnID == conn.ConnID
|
||||||
|
if err := u.msg.OnDisconnect(ctx, conn.EndpointID, conn.ConnID, isCurrent); err != nil {
|
||||||
|
u.log.Error("message disconnect", "endpoint", conn.EndpointID, "err", err)
|
||||||
|
}
|
||||||
|
u.conns.Clear(conn.EndpointID, conn.ConnID)
|
||||||
|
if u.next != nil {
|
||||||
|
u.next.OnDisconnect(ctx, conn, reason)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *appUplink) HandleUplink(ctx context.Context, conn port.ConnInfo, payload []byte) error {
|
||||||
|
if u.next != nil {
|
||||||
|
return u.next.HandleUplink(ctx, conn, payload)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// presenceConnTable 把消息连接表暴露给 presence.ConnTable。
|
||||||
|
type presenceConnTable struct {
|
||||||
|
conns *message.MemoryConns
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *presenceConnTable) IsOnline(endpointID string) bool {
|
||||||
|
_, ok := p.conns.Current(endpointID)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *presenceConnTable) CurrentConn(endpointID string) (port.ConnID, bool) {
|
||||||
|
live, ok := p.conns.Current(endpointID)
|
||||||
|
if !ok {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return live.ConnID, true
|
||||||
|
}
|
||||||
+6
-6
@@ -9,7 +9,7 @@ import (
|
|||||||
"git.asio.asia/nixevol/NixMsg/internal/auth"
|
"git.asio.asia/nixevol/NixMsg/internal/auth"
|
||||||
)
|
)
|
||||||
|
|
||||||
// appDeps 是 serve 组装出的模块依赖。各线在后续任务中替换假实现为真实实现。
|
// appDeps 是组装出的模块依赖(测试与骨架仍可调用 wire)。
|
||||||
type appDeps struct {
|
type appDeps struct {
|
||||||
HashPool auth.HashPool
|
HashPool auth.HashPool
|
||||||
SessionTokens auth.SessionTokens
|
SessionTokens auth.SessionTokens
|
||||||
@@ -26,13 +26,13 @@ type appDeps struct {
|
|||||||
Conns port.ConnControl
|
Conns port.ConnControl
|
||||||
}
|
}
|
||||||
|
|
||||||
// wire 组装各业务模块的骨架依赖(T0.4:假实现;后续各线替换)。
|
// wire 返回真实 auth 实现与空业务桩(无 DB 时的轻量装配;正式 serve 走 assembleRuntime)。
|
||||||
func wire() appDeps {
|
func wire() appDeps {
|
||||||
return appDeps{
|
return appDeps{
|
||||||
HashPool: auth.NewStubHashPool(),
|
HashPool: auth.NewPool(),
|
||||||
SessionTokens: auth.NewStubSessionTokens(),
|
SessionTokens: auth.NewSessionTokens(),
|
||||||
APITokens: auth.NewStubAPITokens(),
|
APITokens: auth.NewAPITokens(),
|
||||||
LoginLocks: auth.NewStubLoginLocks(),
|
LoginLocks: auth.NewLoginLocks(),
|
||||||
|
|
||||||
Messages: message.NewStub(),
|
Messages: message.NewStub(),
|
||||||
Identity: identity.NewStub(),
|
Identity: identity.NewStub(),
|
||||||
|
|||||||
@@ -0,0 +1,313 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/cookiejar"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.asio.asia/nixevol/NixMsg/internal/config"
|
||||||
|
"git.asio.asia/nixevol/NixMsg/internal/protocol"
|
||||||
|
"git.asio.asia/nixevol/NixMsg/internal/store"
|
||||||
|
"git.asio.asia/nixevol/NixMsg/test/harness"
|
||||||
|
"github.com/mochi-mqtt/server/v2/packets"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWireAdminLoginRegisterMQTTHandshake(t *testing.T) {
|
||||||
|
dataDir := t.TempDir()
|
||||||
|
cfgPath := writeTestConfig(t, dataDir)
|
||||||
|
initAdminForTest(t, dataDir)
|
||||||
|
enableRegistration(t, dataDir, "wire-code-99")
|
||||||
|
|
||||||
|
cfg, err := config.Load(cfgPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if vErr := cfg.Validate(); vErr != nil {
|
||||||
|
t.Fatal(vErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
errCh := make(chan error, 1)
|
||||||
|
go func() { errCh <- runServe(ctx, cfg) }()
|
||||||
|
|
||||||
|
addr := waitListenAddr(t, dataDir, 15*time.Second)
|
||||||
|
base := "http://" + addr
|
||||||
|
|
||||||
|
// 1) admin init 后真实进程可管理登录
|
||||||
|
jar, err := cookiejar.New(nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
client := &http.Client{Jar: jar, Timeout: 10 * time.Second}
|
||||||
|
loginBody, _ := json.Marshal(map[string]string{
|
||||||
|
"username": "admin",
|
||||||
|
"password": "test-admin-password-xx",
|
||||||
|
})
|
||||||
|
resp, err := client.Post(base+"/api/admin/login", "application/json", bytes.NewReader(loginBody))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("admin login: %v", err)
|
||||||
|
}
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
t.Fatalf("admin login status=%d body=%s", resp.StatusCode, body)
|
||||||
|
}
|
||||||
|
var loginEnv struct {
|
||||||
|
OK bool `json:"ok"`
|
||||||
|
}
|
||||||
|
if uErr := json.Unmarshal(body, &loginEnv); uErr != nil || !loginEnv.OK {
|
||||||
|
t.Fatalf("admin login resp=%s", body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) 已写入注册开关与安全码后可注册
|
||||||
|
regBody := `{"registration_code":"wire-code-99","id":"ep_wire1","login_password":"password12","name":"接线端"}`
|
||||||
|
regResp, err := http.Post(base+"/api/client/register", "application/json", strings.NewReader(regBody))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("register: %v", err)
|
||||||
|
}
|
||||||
|
regBytes, _ := io.ReadAll(regResp.Body)
|
||||||
|
_ = regResp.Body.Close()
|
||||||
|
if regResp.StatusCode != http.StatusOK {
|
||||||
|
t.Fatalf("register status=%d body=%s", regResp.StatusCode, regBytes)
|
||||||
|
}
|
||||||
|
var regEnv struct {
|
||||||
|
OK bool `json:"ok"`
|
||||||
|
Data struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
if uErr := json.Unmarshal(regBytes, ®Env); uErr != nil || !regEnv.OK || regEnv.Data.ID != "ep_wire1" {
|
||||||
|
t.Fatalf("register resp=%s", regBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) 注册出的端用密码完成 MQTT 握手并拿到 session_token
|
||||||
|
tok := mqttPasswordHandshake(t, base, "ep_wire1", "password12")
|
||||||
|
if tok == "" || !strings.HasPrefix(tok, protocol.SessionTokenPrefix) {
|
||||||
|
t.Fatalf("session_token=%q", tok)
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel()
|
||||||
|
select {
|
||||||
|
case err := <-errCh:
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("serve exit: %v", err)
|
||||||
|
}
|
||||||
|
case <-time.After(15 * time.Second):
|
||||||
|
t.Fatal("serve did not stop")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mqttPasswordHandshake(t *testing.T, httpBase, endpointID, password string) string {
|
||||||
|
t.Helper()
|
||||||
|
mc, err := harness.DialMQTTWebSocket(httpBase, 10*time.Second)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("dial mqtt ws: %v", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = mc.Close() }()
|
||||||
|
|
||||||
|
pk := packets.Packet{
|
||||||
|
FixedHeader: packets.FixedHeader{Type: packets.Connect},
|
||||||
|
ProtocolVersion: 5,
|
||||||
|
Connect: packets.ConnectParams{
|
||||||
|
ProtocolName: []byte("MQTT"),
|
||||||
|
Clean: true,
|
||||||
|
ClientIdentifier: endpointID,
|
||||||
|
Keepalive: 30,
|
||||||
|
UsernameFlag: true,
|
||||||
|
Username: []byte(endpointID),
|
||||||
|
PasswordFlag: true,
|
||||||
|
Password: []byte(password),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if encErr := pk.ConnectEncode(&buf); encErr != nil {
|
||||||
|
t.Fatal(encErr)
|
||||||
|
}
|
||||||
|
if sendErr := mc.Send(buf.Bytes()); sendErr != nil {
|
||||||
|
t.Fatal(sendErr)
|
||||||
|
}
|
||||||
|
ack, err := mc.Recv()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("connack: %v", err)
|
||||||
|
}
|
||||||
|
if len(ack) < 2 || ack[0]>>4 != packets.Connack {
|
||||||
|
t.Fatalf("want CONNACK, got %x", ack)
|
||||||
|
}
|
||||||
|
// MQTT5 CONNACK: remaining length, flags, reason code
|
||||||
|
reason := byte(0)
|
||||||
|
if len(ack) >= 4 {
|
||||||
|
reason = ack[3]
|
||||||
|
}
|
||||||
|
if reason != 0 {
|
||||||
|
t.Fatalf("connack reason=%d raw=%x", reason, ack)
|
||||||
|
}
|
||||||
|
|
||||||
|
sub := packets.Packet{
|
||||||
|
FixedHeader: packets.FixedHeader{Type: packets.Subscribe, Qos: 1},
|
||||||
|
ProtocolVersion: 5,
|
||||||
|
PacketID: 1,
|
||||||
|
Filters: packets.Subscriptions{
|
||||||
|
{Filter: "nix/c/" + endpointID + "/down", Qos: 1},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
buf.Reset()
|
||||||
|
if err := sub.SubscribeEncode(&buf); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := mc.Send(buf.Bytes()); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := mc.Recv(); err != nil { // SUBACK
|
||||||
|
t.Fatalf("suback: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
hello, _ := protocol.Marshal(protocol.Hello{
|
||||||
|
V: protocol.Version, Type: protocol.TypeHello, RID: "h1",
|
||||||
|
})
|
||||||
|
pub := packets.Packet{
|
||||||
|
FixedHeader: packets.FixedHeader{Type: packets.Publish, Qos: 1},
|
||||||
|
ProtocolVersion: 5,
|
||||||
|
TopicName: "nix/c/" + endpointID + "/up",
|
||||||
|
PacketID: 2,
|
||||||
|
Payload: hello,
|
||||||
|
}
|
||||||
|
buf.Reset()
|
||||||
|
if err := pub.PublishEncode(&buf); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := mc.Send(buf.Bytes()); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
deadline := time.Now().Add(10 * time.Second)
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
raw, err := mc.Recv()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("recv down: %v", err)
|
||||||
|
}
|
||||||
|
if len(raw) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
typ := raw[0] >> 4
|
||||||
|
if typ == packets.Puback || typ == packets.Pingresp {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if typ != packets.Publish {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
payload, err := decodePublishPayload(raw)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("publish decode: %v raw=%x", err, raw)
|
||||||
|
}
|
||||||
|
var m map[string]any
|
||||||
|
if err := json.Unmarshal(payload, &m); err != nil {
|
||||||
|
t.Fatalf("json: %v payload=%s", err, payload)
|
||||||
|
}
|
||||||
|
if m["type"] == "resp" {
|
||||||
|
if tok := extractSessionToken(m); tok != "" {
|
||||||
|
return tok
|
||||||
|
}
|
||||||
|
t.Fatalf("hello resp without token: %v", m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Fatal("timeout waiting hello resp")
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodePublishPayload(raw []byte) ([]byte, error) {
|
||||||
|
rem, n, err := decodeRemainingLength(raw[1:])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
body := raw[1+n:]
|
||||||
|
if len(body) != rem {
|
||||||
|
return nil, io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
pk := packets.Packet{
|
||||||
|
ProtocolVersion: 5,
|
||||||
|
FixedHeader: packets.FixedHeader{
|
||||||
|
Type: packets.Publish,
|
||||||
|
Remaining: rem,
|
||||||
|
Qos: (raw[0] >> 1) & 0x3,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := pk.PublishDecode(body); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return pk.Payload, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeRemainingLength(b []byte) (value int, n int, err error) {
|
||||||
|
var mul uint32 = 1
|
||||||
|
var v uint32
|
||||||
|
for i := 0; i < len(b) && i < 4; i++ {
|
||||||
|
v += uint32(b[i]&127) * mul
|
||||||
|
n++
|
||||||
|
if b[i]&128 == 0 {
|
||||||
|
return int(v), n, nil
|
||||||
|
}
|
||||||
|
mul *= 128
|
||||||
|
}
|
||||||
|
return 0, 0, io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractSessionToken(m map[string]any) string {
|
||||||
|
if m["ok"] != true {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
data, _ := m["data"].(map[string]any)
|
||||||
|
tok, _ := data["session_token"].(string)
|
||||||
|
return tok
|
||||||
|
}
|
||||||
|
|
||||||
|
func enableRegistration(t *testing.T, dataDir, code string) {
|
||||||
|
t.Helper()
|
||||||
|
db, err := store.Open(dataDir, "FULL")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer func() { _ = db.Close() }()
|
||||||
|
now := time.Now().UnixMilli()
|
||||||
|
err = db.Queue.Do(context.Background(), func(tx *sql.Tx) error {
|
||||||
|
if _, e := tx.Exec(`INSERT INTO settings(key, value, updated_at) VALUES(?,?,?)
|
||||||
|
ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at`,
|
||||||
|
"registration_enabled", "1", now); e != nil {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
_, e := tx.Exec(`INSERT INTO settings(key, value, updated_at) VALUES(?,?,?)
|
||||||
|
ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at`,
|
||||||
|
"registration_code", code, now)
|
||||||
|
return e
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitListenAddr(t *testing.T, dataDir string, timeout time.Duration) string {
|
||||||
|
t.Helper()
|
||||||
|
deadline := time.Now().Add(timeout)
|
||||||
|
path := filepath.Join(dataDir, "listen.addr")
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
b, err := os.ReadFile(path)
|
||||||
|
if err == nil {
|
||||||
|
addr := strings.TrimSpace(string(b))
|
||||||
|
if addr != "" {
|
||||||
|
return addr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
time.Sleep(20 * time.Millisecond)
|
||||||
|
}
|
||||||
|
t.Fatal("listen.addr not written")
|
||||||
|
return ""
|
||||||
|
}
|
||||||
@@ -43,6 +43,29 @@
|
|||||||
- 备选方案:docker 目标仅 echo 提示。
|
- 备选方案:docker 目标仅 echo 提示。
|
||||||
- 影响:镜像发布流程仍由 Q4 定稿。
|
- 影响:镜像发布流程仍由 Q4 定稿。
|
||||||
|
|
||||||
|
### L-WIRE 2026-09-30
|
||||||
|
|
||||||
|
1. **serve 真实接线范围**
|
||||||
|
- 原条款:TASKS 总控接线;listener/broker/admin/注册/消息周期循环挂到进程。
|
||||||
|
- 实际做法:`cmd/nixmsg/serve.go` 注入真实 `auth.NewPool`/`NewSessionTokens`/`NewAPITokens`/`NewLoginLocks`,挂注册与管理路由(踢线调 `Session.Kick`),listener 识别 HTTP/WebSocket/`OnMQTT` 裸 TCP,broker `Login`+`Session`,握手/断线/`OnPublishDropped` 接到 `message.App`,周期 `DispatchDue`/`PushPending`/`CleanupOnce`,下行 `PublishDown`。集成测覆盖:管理登录、写 settings 后注册、密码 MQTT 握手拿 `session_token`。
|
||||||
|
- 原因:第二波收尾;三件验收必须通。
|
||||||
|
- 备选方案:分文件多阶段接线。
|
||||||
|
- 影响:进程可对外登录/注册/握手。
|
||||||
|
|
||||||
|
2. **未接线 / 未完成部分**
|
||||||
|
- 原条款:上行应用帧完整分发到 identity/group/presence/message 业务方法。
|
||||||
|
- 实际做法:`Session` 处理 hello/logout;其余 `HandleUplink` 仍为 `StubUplinkHandler`(不解析 send/ack/group 等)。管理注册设置 HTTP(A3)未实现,测试直接写 `settings` 表。群/在线模块已构造并注入 Downlink,但无上行入口调用。
|
||||||
|
- 原因:本波强制三件验收;完整协议分发属后续波次。
|
||||||
|
- 备选方案:本波同时实现 HandleUplink 大 multiplex。
|
||||||
|
- 影响:端连上后除握手/logout 外业务帧尚无 resp;F03–F16 等仍依赖后续接线。
|
||||||
|
|
||||||
|
3. **listen.addr 始终写入**
|
||||||
|
- 原条款:listener N1 仅端口 0 写文件;T0.1 超集为启动即写。
|
||||||
|
- 实际做法:listener 按 N1 写端口 0;serve 成功后再强制写一次 `listen.addr`(及分离时的 `admin.addr`)。
|
||||||
|
- 原因:与 harness / T0.1 一致。
|
||||||
|
- 备选方案:改 listener 始终写。
|
||||||
|
- 影响:固定端口也会有地址文件。
|
||||||
|
|
||||||
### T0.2 2026-09-30
|
### T0.2 2026-09-30
|
||||||
|
|
||||||
1. **请求指纹规范化格式**
|
1. **请求指纹规范化格式**
|
||||||
|
|||||||
@@ -67,6 +67,17 @@ func (c *MemoryConns) Current(endpointID string) (LiveConn, bool) {
|
|||||||
return v, ok
|
return v, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Snapshot 返回当前连接表副本(供推送循环遍历)。
|
||||||
|
func (c *MemoryConns) Snapshot() map[string]LiveConn {
|
||||||
|
c.mu.RLock()
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
out := make(map[string]LiveConn, len(c.m))
|
||||||
|
for k, v := range c.m {
|
||||||
|
out[k] = v
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// RecordingDownlink 记录下行发布,供测试断言。
|
// RecordingDownlink 记录下行发布,供测试断言。
|
||||||
type RecordingDownlink struct {
|
type RecordingDownlink struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
|
|||||||
@@ -58,11 +58,16 @@ func (AllowAuthenticator) Authenticate(context.Context, string, []byte, string)
|
|||||||
return AuthResult{OK: true}, nil
|
return AuthResult{OK: true}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PublishDroppedFunc 下行未写入发送队列时回调(对接消息线 OnPublishDropped)。
|
||||||
|
type PublishDroppedFunc func(ctx context.Context, endpointID string, connID port.ConnID, payload []byte)
|
||||||
|
|
||||||
// Options 装配 broker。
|
// Options 装配 broker。
|
||||||
type Options struct {
|
type Options struct {
|
||||||
Authenticator Authenticator
|
Authenticator Authenticator
|
||||||
Uplink port.UplinkHandler
|
Uplink port.UplinkHandler
|
||||||
Logger *slog.Logger
|
Logger *slog.Logger
|
||||||
|
// OnPublishDropped 可选;nil 时仅打 debug 日志。
|
||||||
|
OnPublishDropped PublishDroppedFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
// Broker 内置 mochi,不自带监听端口。
|
// Broker 内置 mochi,不自带监听端口。
|
||||||
@@ -71,6 +76,7 @@ type Broker struct {
|
|||||||
auth Authenticator
|
auth Authenticator
|
||||||
uplink port.UplinkHandler
|
uplink port.UplinkHandler
|
||||||
log *slog.Logger
|
log *slog.Logger
|
||||||
|
onDrop PublishDroppedFunc
|
||||||
|
|
||||||
hook *nixHook
|
hook *nixHook
|
||||||
|
|
||||||
@@ -144,6 +150,7 @@ func New(opts Options) (*Broker, error) {
|
|||||||
auth: auth,
|
auth: auth,
|
||||||
uplink: uplink,
|
uplink: uplink,
|
||||||
log: log,
|
log: log,
|
||||||
|
onDrop: opts.OnPublishDropped,
|
||||||
current: make(map[string]*connState),
|
current: make(map[string]*connState),
|
||||||
byClient: make(map[*mqtt.Client]*connState),
|
byClient: make(map[*mqtt.Client]*connState),
|
||||||
queues: make(map[string]*uplinkQueue),
|
queues: make(map[string]*uplinkQueue),
|
||||||
|
|||||||
@@ -159,6 +159,16 @@ func (h *nixHook) OnSubscribed(cl *mqtt.Client, pk packets.Packet, reasonCodes [
|
|||||||
|
|
||||||
func (h *nixHook) OnPublishDropped(cl *mqtt.Client, pk packets.Packet) {
|
func (h *nixHook) OnPublishDropped(cl *mqtt.Client, pk packets.Packet) {
|
||||||
h.b.log.Debug("publish dropped", "client", cl.ID, "topic", pk.TopicName, "size", len(pk.Payload))
|
h.b.log.Debug("publish dropped", "client", cl.ID, "topic", pk.TopicName, "size", len(pk.Payload))
|
||||||
|
if h.b.onDrop == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.b.connsMu.RLock()
|
||||||
|
st := h.b.byClient[cl]
|
||||||
|
h.b.connsMu.RUnlock()
|
||||||
|
if st == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.b.onDrop(context.Background(), st.endpointID, st.connID, append([]byte(nil), pk.Payload...))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *nixHook) OnSessionEstablished(cl *mqtt.Client, _ packets.Packet) {
|
func (h *nixHook) OnSessionEstablished(cl *mqtt.Client, _ packets.Packet) {
|
||||||
|
|||||||
@@ -101,6 +101,11 @@ func (s *Session) Attach(b *Broker) {
|
|||||||
s.b = b
|
s.b = b
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetPresence 接线时在创建最终 presence 实现后注入(可替换占位)。
|
||||||
|
func (s *Session) SetPresence(p PresenceSink) {
|
||||||
|
s.presence = p
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Session) OnSessionEstablished(ctx context.Context, conn port.ConnInfo) error {
|
func (s *Session) OnSessionEstablished(ctx context.Context, conn port.ConnInfo) error {
|
||||||
if s.b != nil {
|
if s.b != nil {
|
||||||
s.b.startHandshakeDeadline(conn.EndpointID, conn.ConnID, handshakeTimeout)
|
s.b.startHandshakeDeadline(conn.EndpointID, conn.ConnID, handshakeTimeout)
|
||||||
|
|||||||
Reference in New Issue
Block a user