package bus import ( "context" "errors" "expvar" "fmt" "io" "log" "log/slog" "net" "net/http" "strings" "sync" "time" commonv1 "github.com/johnandersen777/socialweb-computer-kcp/api/common/v1alpha1" contract "github.com/johnandersen777/socialweb-computer-kcp/internal/contract/component" "github.com/johnandersen777/socialweb-computer-kcp/internal/kit/hostport" "github.com/johnandersen777/socialweb-computer-kcp/internal/kit/kcp" "github.com/johnandersen777/socialweb-computer-kcp/internal/kit/supervise" ) const DefaultHandlerName = "http" var ( ErrNoHandler = errors.New("bus: no HTTP handler is registered") ErrNoStream = errors.New("bus: no stream handler is registered") ErrHandlersStopped = errors.New("bus: the handler registry is stopped") ) type StreamHandler func(ctx context.Context, conn net.Conn) error type Bus struct { mu sync.RWMutex handlers map[Key]map[string]http.Handler revisions map[Key]map[string]uint64 aliases map[string]handlerAlias streams map[streamKey]StreamHandler conns map[*trackedConn]Key links []Link changed chan struct{} providers map[providerKey]provider subMu sync.Mutex subs map[uint64]func(Key) nextSub uint64 denials denials root context.Context cancel context.CancelFunc closed bool served sync.WaitGroup transport *http.Transport } const LinkSettle = 5 * time.Second type HandlerRegistry = Bus type Handlers = Bus type Key = contract.Key type handlerAlias struct { key Key name string ambiguous bool } type streamKey struct { Key name string } func New(ctx context.Context) *Bus { root, cancel := context.WithCancel(context.WithoutCancel(ctx)) r := &Bus{ handlers: map[Key]map[string]http.Handler{}, revisions: map[Key]map[string]uint64{}, aliases: map[string]handlerAlias{}, streams: map[streamKey]StreamHandler{}, conns: map[*trackedConn]Key{}, providers: map[providerKey]provider{}, root: root, cancel: cancel, } r.denials.changed = make(chan struct{}, 1) r.transport = &http.Transport{ Proxy: nil, DialContext: func(ctx context.Context, network, address string) (net.Conn, error) { return r.dialHTTP(ctx, network, address) }, } return r } func NewHandlerRegistry(ctx context.Context) *Bus { return New(ctx) } func (r *Bus) Subscribe(fn func(Key)) (cancel func()) { if r == nil || fn == nil { return func() {} } r.subMu.Lock() if r.subs == nil { r.subs = map[uint64]func(Key){} } r.nextSub++ id := r.nextSub r.subs[id] = fn r.subMu.Unlock() var once sync.Once return func() { once.Do(func() { r.subMu.Lock() delete(r.subs, id) r.subMu.Unlock() }) } } func (r *Bus) notify(key Key) { r.subMu.Lock() fns := make([]func(Key), 0, len(r.subs)) for _, fn := range r.subs { fns = append(fns, fn) } r.subMu.Unlock() for _, fn := range fns { fn(key) } } func (r *Bus) NotifyBound(key Key) { if r == nil { return } r.notify(key) } func NewHandlers(ctx context.Context) *Bus { return New(ctx) } func (r *Bus) SetLogger(log *slog.Logger) { if r == nil { return } r.denials.log.Store(log) } func (r *Bus) Denied() uint64 { if r == nil { return 0 } return r.denials.count.Load() } func (r *Bus) DeniedByCapability() map[string]uint64 { out := map[string]uint64{} if r == nil { return out } r.denials.byCapability.Do(func(kv expvar.KeyValue) { if n, ok := kv.Value.(*expvar.Int); ok { out[kv.Key] = uint64(n.Value()) } }) return out } func (r *Bus) DeniedChanged() <-chan struct{} { if r == nil { return nil } return r.denials.changed } func (r *Bus) Client() *http.Client { if r == nil { return &http.Client{Transport: roundTripperFunc(func(*http.Request) (*http.Response, error) { return nil, ErrHandlersStopped })} } return &http.Client{Transport: r.RoundTripper()} } func (r *Bus) HTTPClient() *http.Client { return r.Client() } func (r *Bus) RoundTripper() http.RoundTripper { return &handlerRoundTripper{registry: r} } func (r *Bus) SetLinks(links []Link) { if r == nil { return } next := cloneLinks(links) r.mu.Lock() if linksEqual(r.links, next) { r.mu.Unlock() return } r.links = next if r.changed != nil { close(r.changed) r.changed = nil } stale := make([]*trackedConn, 0, len(r.conns)) for conn, key := range r.conns { if conn.granted && allowed(next, conn.caller, key.Ref(), conn.grant) { continue } delete(r.conns, conn) stale = append(stale, conn) } r.mu.Unlock() closeTracked(stale) } func (r *Bus) Links() []Link { if r == nil { return nil } r.mu.RLock() defer r.mu.RUnlock() return cloneLinks(r.links) } func (r *Bus) RegisterHTTP(key Key, handler http.Handler, name ...string) error { return r.RegisterHTTPNamed(key, optionalName(name), handler) } func (r *Bus) RegisterHTTPNamed(key Key, name string, handler http.Handler) error { if r == nil { return errors.New("bus: the handler registry is nil") } if handler == nil { return fmt.Errorf("bus: the HTTP handler for %s/%s is nil", key.Kind, key.Name) } if strings.TrimSpace(key.Kind) == "" || strings.TrimSpace(key.Name) == "" { return errors.New("bus: an HTTP handler key needs a kind and a name") } name = normalizeHandlerName(name) r.mu.Lock() if r.closed { r.mu.Unlock() return ErrHandlersStopped } if r.handlers == nil { r.handlers = map[Key]map[string]http.Handler{} } names := r.handlers[key] if names == nil { names = map[string]http.Handler{} r.handlers[key] = names } names[name] = handler revs := r.revisions[key] if revs == nil { revs = map[string]uint64{} r.revisions[key] = revs } revs[name]++ r.rebuildAliasesLocked() stale := r.takeConnsLocked(key) r.mu.Unlock() closeTracked(stale) r.notify(key) return nil } func (r *Bus) Handler(key Key, name ...string) (http.Handler, bool) { return r.HandlerNamed(key, optionalName(name)) } func (r *Bus) HandlerRevision(key Key, name ...string) uint64 { if r == nil { return 0 } r.mu.RLock() defer r.mu.RUnlock() return r.revisions[key][normalizeHandlerName(optionalName(name))] } func (r *Bus) HandlerNamed(key Key, name string) (http.Handler, bool) { if r == nil { return nil, false } name = normalizeHandlerName(name) r.mu.RLock() handler := r.handlers[key][name] r.mu.RUnlock() return handler, handler != nil } func (r *Bus) HandlerForHost(host string) (http.Handler, bool) { handler, _, _, ok := r.handlerForHost(host) return handler, ok } func (r *Bus) DialHTTP(ctx context.Context, address string) (net.Conn, error) { if err := r.Ready(ctx); err != nil { return nil, err } return r.dialHTTP(ctx, "tcp", address) } func (r *Bus) Remove(key Key) bool { if r == nil { return false } r.mu.Lock() removed := false if _, ok := r.handlers[key]; ok { delete(r.handlers, key) delete(r.revisions, key) removed = true } for streamKey := range r.streams { if streamKey.Key == key { delete(r.streams, streamKey) removed = true } } if removed { r.rebuildAliasesLocked() } stale := r.takeConnsLocked(key) r.mu.Unlock() closeTracked(stale) if removed { r.notify(key) } return removed } func (r *Bus) RegisterStream(key Key, handler StreamHandler, name ...string) error { if r == nil { return errors.New("bus: the handler registry is nil") } if handler == nil { return fmt.Errorf("bus: the stream handler for %s/%s is nil", key.Kind, key.Name) } if strings.TrimSpace(key.Kind) == "" || strings.TrimSpace(key.Name) == "" { return errors.New("bus: a stream handler key needs a kind and a name") } sk := streamKey{Key: key, name: normalizeHandlerName(optionalName(name))} r.mu.Lock() if r.closed { r.mu.Unlock() return ErrHandlersStopped } if r.streams == nil { r.streams = map[streamKey]StreamHandler{} } r.streams[sk] = handler stale := r.takeConnsLocked(key) r.mu.Unlock() closeTracked(stale) r.notify(key) return nil } func (r *Bus) DialStream(ctx context.Context, key Key, name ...string) (net.Conn, error) { if r == nil { return nil, ErrHandlersStopped } handlerName := normalizeHandlerName(optionalName(name)) grant := Capability{Name: CapabilityStream, Scope: []string{handlerName}} if err := r.authorize(ctx, key.Ref(), grant); err != nil { return nil, err } caller, hasCaller := CallerFrom(ctx) sk := streamKey{Key: key, name: handlerName} r.mu.RLock() if r.closed { r.mu.RUnlock() return nil, ErrHandlersStopped } handler := r.streams[sk] root := r.root r.mu.RUnlock() if handler == nil { return nil, fmt.Errorf("%w: %s/%s", ErrNoStream, key.Kind, key.Name) } client, server := net.Pipe() clientConn, err := r.trackGranted(client, key, caller, hasCaller, grant) if err != nil { server.Close() return nil, err } serverConn, err := r.trackGranted(server, key, caller, hasCaller, grant) if err != nil { clientConn.Close() server.Close() return nil, err } streamCtx, cancel := context.WithCancel(ctx) stopRoot := context.AfterFunc(root, cancel) clientConn.addHook(func() { stopRoot() }) stopClient := context.AfterFunc(ctx, func() { clientConn.Close() }) clientConn.addHook(func() { stopClient() }) if !r.goServe(func() { defer cancel() defer serverConn.Close() _ = handler(streamCtx, serverConn) }) { cancel() clientConn.Close() serverConn.Close() return nil, ErrHandlersStopped } return clientConn, nil } func (r *Bus) RemoveStream(key Key, name ...string) bool { if r == nil { return false } sk := streamKey{Key: key, name: normalizeHandlerName(optionalName(name))} r.mu.Lock() _, ok := r.streams[sk] delete(r.streams, sk) stale := r.takeConnsLocked(key) r.mu.Unlock() closeTracked(stale) if ok { r.notify(key) } return ok } func (r *Bus) Ready(ctx ...context.Context) error { if r == nil { return ErrHandlersStopped } if len(ctx) > 0 { if err := ctx[0].Err(); err != nil { return err } } r.mu.RLock() closed := r.closed r.mu.RUnlock() if closed { return ErrHandlersStopped } return nil } func (r *Bus) Stop(ctx ...context.Context) error { if r == nil { return nil } r.mu.Lock() if r.closed { r.mu.Unlock() return nil } r.closed = true if r.cancel != nil { r.cancel() } stale := make([]*trackedConn, 0, len(r.conns)) for conn := range r.conns { stale = append(stale, conn) } r.conns = map[*trackedConn]Key{} r.handlers = map[Key]map[string]http.Handler{} r.streams = map[streamKey]StreamHandler{} r.aliases = map[string]handlerAlias{} r.mu.Unlock() closeTracked(stale) if len(ctx) > 0 { return supervise.Join(ctx[0], r.served.Wait) } r.served.Wait() return nil } func (r *Bus) goServe(fn func()) bool { r.mu.Lock() defer r.mu.Unlock() if r.closed { return false } r.served.Go(fn) return true } func (r *Bus) Close() error { return r.Stop() } func (r *Bus) Closed() bool { if r == nil { return true } r.mu.RLock() defer r.mu.RUnlock() return r.closed } func (r *Bus) dialHTTP(ctx context.Context, _ string, address string) (net.Conn, error) { if err := r.Ready(); err != nil { return nil, err } host := hostport.Normalize(address) handler, key, _, ok := r.handlerForHost(host) if !ok { return nil, fmt.Errorf("%w: %s", ErrNoHandler, host) } grant := Capability{Name: CapabilityXRPC} if authorized, ok := ctx.Value(authorizedDialKey{}).(authorizedDial); ok && authorized.key == key { grant = authorized.grant } else if err := r.authorize(ctx, key.Ref(), grant); err != nil { return nil, err } caller, hasCaller := CallerFrom(ctx) client, server := net.Pipe() clientConn, err := r.trackGranted(client, key, caller, hasCaller, grant) if err != nil { server.Close() return nil, err } serverConn, err := r.trackGranted(server, key, caller, hasCaller, grant) if err != nil { clientConn.Close() server.Close() return nil, err } serverConn.caller = caller serverConn.hasCaller = hasCaller serveCtx, cancel := context.WithCancel(r.rootContext()) if !r.goServe(func() { defer cancel() r.serveHTTP(serveCtx, serverConn, handler) }) { cancel() clientConn.Close() serverConn.Close() return nil, ErrHandlersStopped } return clientConn, nil } func (r *Bus) serveHTTP(ctx context.Context, conn *trackedConn, handler http.Handler) { defer conn.Close() if conn.hasCaller { ctx = context.WithValue(ctx, callerKey{}, conn.caller) } server := &http.Server{ Handler: handler, BaseContext: func(net.Listener) context.Context { return ctx }, ErrorLog: log.New(io.Discard, "", 0), } _ = server.Serve(newSingleConnListener(conn)) } func (r *Bus) handlerForHost(host string) (http.Handler, Key, string, bool) { if r == nil { return nil, Key{}, "", false } host = hostport.Normalize(host) r.mu.RLock() alias, ok := r.aliases[host] if ok && !alias.ambiguous { handler := r.handlers[alias.key][alias.name] r.mu.RUnlock() return handler, alias.key, alias.name, handler != nil } r.mu.RUnlock() return nil, Key{}, "", false } func (r *Bus) rootContext() context.Context { r.mu.RLock() defer r.mu.RUnlock() return r.root } func (r *Bus) rebuildAliasesLocked() { aliases := make(map[string]handlerAlias, len(r.aliases)) for key, names := range r.handlers { for name := range names { for _, host := range handlerAliases(key, name) { current, ok := aliases[host] if !ok { aliases[host] = handlerAlias{key: key, name: name} continue } if current.key != key || current.name != name { current.ambiguous = true aliases[host] = current } } } } r.aliases = aliases } func (r *Bus) trackConn(conn net.Conn, key Key) (*trackedConn, error) { tracked := &trackedConn{Conn: conn, done: make(chan struct{})} tracked.onClose = func() { r.mu.Lock() delete(r.conns, tracked) r.mu.Unlock() } r.mu.Lock() if r.closed { r.mu.Unlock() conn.Close() return nil, ErrHandlersStopped } if r.conns == nil { r.conns = map[*trackedConn]Key{} } r.conns[tracked] = key r.mu.Unlock() return tracked, nil } func (r *Bus) trackGranted(conn net.Conn, key Key, caller Caller, hasCaller bool, grant Capability) (*trackedConn, error) { tracked, err := r.trackConn(conn, key) if err != nil { return nil, err } r.mu.Lock() tracked.caller, tracked.grant, tracked.granted = caller, grant, hasCaller r.mu.Unlock() return tracked, nil } func (r *Bus) takeConnsLocked(key Key) []*trackedConn { out := make([]*trackedConn, 0, len(r.conns)) for conn, connKey := range r.conns { if connKey != key { continue } delete(r.conns, conn) out = append(out, conn) } return out } type trackedConn struct { net.Conn done chan struct{} once sync.Once mu sync.Mutex hooks []func() closed bool caller Caller hasCaller bool grant Capability granted bool onClose func() } func (c *trackedConn) addHook(hook func()) { if hook == nil { return } c.mu.Lock() if c.closed { c.mu.Unlock() hook() return } c.hooks = append(c.hooks, hook) c.mu.Unlock() } func (c *trackedConn) Close() error { err := c.Conn.Close() c.once.Do(func() { if c.onClose != nil { c.onClose() } c.mu.Lock() c.closed = true hooks := c.hooks c.hooks = nil c.mu.Unlock() for _, hook := range hooks { hook() } close(c.done) }) return err } type singleConnListener struct { conn net.Conn done <-chan struct{} mu sync.Mutex served bool } func newSingleConnListener(conn *trackedConn) *singleConnListener { return &singleConnListener{conn: conn, done: conn.done} } func (l *singleConnListener) Accept() (net.Conn, error) { l.mu.Lock() if !l.served { l.served = true conn := l.conn l.mu.Unlock() return conn, nil } l.mu.Unlock() <-l.done return nil, net.ErrClosed } func (l *singleConnListener) Close() error { return nil } func (l *singleConnListener) Addr() net.Addr { return l.conn.LocalAddr() } type handlerRoundTripper struct { registry *Bus } func (t *handlerRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { if t == nil || t.registry == nil { return nil, ErrHandlersStopped } if req == nil || req.URL == nil { return nil, errors.New("bus: the HTTP request has no URL") } if req.URL.Scheme != "http" { return nil, fmt.Errorf("bus: the in-process transport does not serve the scheme %q", req.URL.Scheme) } if err := t.registry.Ready(); err != nil { return nil, err } host := hostport.Normalize(requestHost(req)) _, key, _, ok := t.registry.handlerForHost(host) if !ok { return nil, fmt.Errorf("%w: %s", ErrNoHandler, host) } grant := xrpcCapability(req) if err := t.registry.authorize(req.Context(), key.Ref(), grant); err != nil { return nil, err } next := req.Clone(context.WithValue(req.Context(), authorizedDialKey{}, authorizedDial{key: key, grant: grant})) next.Close = true return t.registry.transport.RoundTrip(next) } type authorizedDialKey struct{} type authorizedDial struct { key Key grant Capability } type roundTripperFunc func(*http.Request) (*http.Response, error) func (f roundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) { return f(req) } func requestHost(req *http.Request) string { if req.URL != nil && req.URL.Host != "" { return req.URL.Host } return req.Host } func xrpcCapability(req *http.Request) Capability { if req != nil && req.URL != nil { if nsid, ok := strings.CutPrefix(req.URL.Path, "/xrpc/"); ok && nsid != "" && !strings.Contains(nsid, "/") { return Capability{Name: CapabilityXRPC, Scope: []string{nsid}} } } return Capability{Name: CapabilityXRPC} } func (r *Bus) authorize(ctx context.Context, to commonv1.ObjectRef, c Capability) error { if r == nil { return ErrHandlersStopped } from, ok := CallerFrom(ctx) if !ok { return r.denials.deny(Caller{}, to, c) } return r.authorizeCaller(ctx, from, to, c) } func (r *Bus) authorizeCaller(ctx context.Context, from Caller, to commonv1.ObjectRef, c Capability) error { wait := time.NewTimer(LinkSettle) defer wait.Stop() for { r.mu.Lock() links := r.links if r.changed == nil { r.changed = make(chan struct{}) } changed := r.changed r.mu.Unlock() if allowed(links, from, to, c) { return nil } if from.System() || !from.Complete() { return r.denials.deny(from, to, c) } select { case <-changed: case <-wait.C: return r.denials.deny(from, to, c) case <-ctx.Done(): return r.denials.deny(from, to, c) } } } func allowed(links []Link, from Caller, to commonv1.ObjectRef, c Capability) bool { if from.System() { return BootstrapCaller(from.Ref.Name) } if !from.Complete() { return false } if refMatches(from.Ref, to) { return true } for _, link := range links { if LinkAllows(link, from, to, c) { return true } } return false } func handlerAliases(key Key, name string) []string { kind := hostport.Normalize(key.Kind) component := hostport.Normalize(key.Name) if kind == "" || component == "" { return nil } bases := []string{kind, kind + "." + component} if workspace := strings.TrimSpace(key.Workspace); workspace != "" { bases = append(bases, kcp.ServiceFQDN(kind, "default", workspace, kcp.DefaultServiceDomain)) } out := make([]string, 0, len(bases)) for _, base := range bases { if name != DefaultHandlerName { base = "_" + name + "." + base } out = append(out, hostport.Normalize(base)) } return out } func normalizeHandlerName(name string) string { name = strings.ToLower(strings.TrimSpace(name)) if name == "" { return DefaultHandlerName } return name } func optionalName(names []string) string { if len(names) == 0 { return "" } return names[0] } func closeTracked(conns []*trackedConn) { for _, conn := range conns { conn.Close() } } func (r *Bus) Transport(external http.RoundTripper) http.RoundTripper { return handlerTransport{bus: r, external: external} } type handlerTransport struct { bus *Bus external http.RoundTripper } func (t handlerTransport) RoundTrip(req *http.Request) (*http.Response, error) { if req != nil && req.URL != nil && req.URL.Scheme == "http" && t.bus != nil { if _, ok := t.bus.HandlerForHost(req.URL.Host); ok { return t.bus.RoundTripper().RoundTrip(req) } } external := t.external if external == nil { external = http.DefaultTransport } return external.RoundTrip(req) }