~samwhited/xmpp

6fa18307357310c975c6209c3110e4f610746e43 — Sam Whited 3 months ago c309fcc
design: update service discovery proposal

The previous plan to include a registry was a rather naive
implementation that would have required that the multiplexer and the
registry both have features registered against them, resulting in a lot
of boilerplate and potential for advertising features that don't exist
or failing to advertise features that do exist because we forgot one or
the other registration.

A new plan was created that uses the multiplexer as the registery so
that handlers regstered against the mux that also advertise features
can automatically respond to disco info requests.
The new plan has the added benefit of working with nested multiplexers
without further modification.

Signed-off-by: Sam Whited <sam@samwhited.com>
4 files changed, 148 insertions(+), 64 deletions(-)

M design/28_disco.md
A disco/features.go
A disco/handler.go
A muc/disco.go
M design/28_disco.md => design/28_disco.md +34 -64
@@ 1,23 1,23 @@
# Proposal: Implement service discovery

**Author(s):** Sam Whited  
**Last updated:** 2020-11-15  
**Last updated:** 2021-08-10  
**Discussion:** https://mellium.im/issue/28


## Abstract

An API should be designed to handle service discovery that integrates with the
[`mux`] package.
An API should be designed to handle responding to service discovery requests
that integrates with the [`mux`] package.


## Background

Even the simplest client or server needs to discover information about other
entities on the network.
Because of this adding an API for service discovery improves our user experience
for almost all XMPP related projects including clients, servers, bots, and
embedded devices.
Because of this adding an API for responding to service discovery improves our
user experience for almost all XMPP related projects including clients, servers,
bots, and embedded devices.
In the XMPP world service discovery is handled by [XEP-0030: Service Discovery]
and augmented by [XEP-0115: Entity Capabilities], but only the first will be
targeted by this design.


@@ 25,83 25,53 @@ targeted by this design.

## Requirements

 - Ability to query for disco info and items
 - An API to walk the info and item tree
 - Ability to register features and items to a registry that can respond to
   disco info and disco items requests
 - Ability to register handlers on a multiplexer and have a registry of features
   automatically created from the set of handlers
 - Implementation must not preclude the addition of XEP-0115: Entity
   Capabilities support at a later date
 - Implementation must not preclude the addition of items or XEP-0115 support at
   a later date


## Proposal

A new package, `disco` will be created to handle registering features and
responding to disco info and items requests.
This package will comprise two new types, one new method, and 4 new functions
that will need to remain backwards compatible once we reach 1.0.
Predefined categories from the [disco categories registry][registry] may also be
generated but will not follow the normal compatibility promise since they will
be kept up to date with the registry.
A new package, `disco/info` will be created and `disco.Feature` will be moved to
`info.Feature` to avoid import loops between the `mux` and `disco` packages.
This is a breaking API change, but is acceptable as we are currently before
version 1.0.

```go
// A Registry is used to register features supported by a server.
type Registry struct {}
The info package will contain interfaces that can be implemented by handlers
providing features:

// NewRegistry creates a new feature registry with the provided identities and
```go
// FeatureIter is the interface implemented by types that implement disco
// features.
// If multiple identities are specified, the name of the registry will be used
// for all of them.
func NewRegistry(...Option) *Registry {}

// HandleIQ handles disco info and item requests.
func (*Registry) HandleIQ(stanza.IQ, xmlstream.TokenReadEncoder, *xml.StartElement) error {}

// An Option is used to configure new registries.
type Option func(*Registry)

// Identity adds an identity to the registry.
func Identity(category, typ, name, lang string) Option {}

// Feature adds a feature to the registry.
func Feature(name string) Option {}

// Merge adds all features from the provided registry into the registry that the
// option is applied to.
func Merge(*Registry) Option {}
type FeatureIter interface{
  ForFeatures(node string, f func(Feature) error) error
}
```

To make constructing a service discovery registry easier, the [`mux`] package
will also be modified with a new interface that can be implemented by handlers
to automatically register themselves with a built in disco registry.
This will require one new type, one new function, and one new method that will
need to remain backwards compatible once we reach version 1.0.
To make responding to service discovery requests easier, the [`mux`] package
will also be modified to implement this new interface.

```go
// DiscoHandler is the type implemented by handlers that can be registered in a
// service discovery registry.
type DiscoHandler interface {
	Disco(*disco.Registry)
}

// Disco adds the provided options to the built in service discovery registry
// and then responds to disco info requests using the registry.
func Disco(opts ...disco.Option) Option {}

// DiscoRegistry returns a service discovery registry containing every feature
// and item from handlers registered on the mux that also supports the
// DiscoHandler interface.
func (*ServeMux) DiscoRegistry() *disco.Registry {}
// ForFeatures implements info.FeatureIter for the mux by iterating over all
// child features.
func (m *ServeMux) ForFeatures(node string, f func(info.Feature) error) error
```

Finally, the `disco` package will be given a new `muc.Option` that responds to
disco requests by calling `m.ForFeatures` method:

## Open Questions
```go
// Handle returns an option that configures a multiplexer to handle service
// discovery requests by iterating over its own handlers and checking if they
// implement info.FeatureIter.
func Handle() mux.Option
```

- How do we handle disco items requests?
Overall this will result in 1 new type, one new method, and one new function
that will need to remain backwards compatible once we reach 1.0.


[`mux`]: https://pkg.go.dev/mellium.im/xmpp/mux
[XEP-0030: Service Discovery]: https://xmpp.org/extensions/xep-0030.html
[XEP-0115: Entity Capabilities]: https://xmpp.org/extensions/xep-0115.html
[registry]: https://xmpp.org/registrar/disco-categories.html

A disco/features.go => disco/features.go +25 -0
@@ 0,0 1,25 @@
// Code generated by "genfeature -filename features.go -receiver h *discoHandler -vars Feature:NSInfo"; DO NOT EDIT.

package disco

import (
	"mellium.im/xmpp/disco/info"
)

// A list of service discovery features that are supported by this package.
var (
	Feature = info.Feature{Var: NSInfo}
)

// ForFeatures implements info.FeatureIter.
func (h *discoHandler) ForFeatures(node string, f func(info.Feature) error) error {
	if node != "" {
		return nil
	}
	var err error
	err = f(Feature)
	if err != nil {
		return err
	}
	return nil
}

A disco/handler.go => disco/handler.go +64 -0
@@ 0,0 1,64 @@
// Copyright 2021 The Mellium Contributors.
// Use of this source code is governed by the BSD 2-clause
// license that can be found in the LICENSE file.

package disco

import (
	"encoding/xml"

	"mellium.im/xmlstream"
	"mellium.im/xmpp/disco/info"
	"mellium.im/xmpp/mux"
	"mellium.im/xmpp/stanza"
)

// Handle returns an option that configures a multiplexer to handle service
// discovery requests by iterating over its own handlers and checking if they
// implement the interfaces from the info package.
func Handle() mux.Option {
	return func(m *mux.ServeMux) {
		h := &discoHandler{ServeMux: m}
		mux.IQ(stanza.GetIQ, xml.Name{Space: NSInfo, Local: "query"}, h)(m)
	}
}

type discoHandler struct {
	*mux.ServeMux
}

func (h *discoHandler) HandleXMPP(t xmlstream.TokenReadEncoder, start *xml.StartElement) error {
	return h.ServeMux.HandleXMPP(t, start)
}

func (h *discoHandler) HandleIQ(iq stanza.IQ, r xmlstream.TokenReadEncoder, start *xml.StartElement) error {
	seen := make(map[string]struct{})
	pr, pw := xmlstream.Pipe()
	go func() {
		switch start.Name.Space {
		case NSInfo:
			var node string
			for _, attr := range start.Attr {
				if attr.Name.Local == "node" {
					node = attr.Value
					break
				}
			}
			pw.CloseWithError(h.ServeMux.ForFeatures(node, func(f info.Feature) error {
				_, ok := seen[f.Var]
				if ok {
					return nil
				}
				seen[f.Var] = struct{}{}
				_, err := xmlstream.Copy(pw, f.TokenReader())
				return err
			}))
		}
	}()

	_, err := xmlstream.Copy(r, iq.Result(xmlstream.Wrap(
		pr,
		*start,
	)))
	return err
}

A muc/disco.go => muc/disco.go +25 -0
@@ 0,0 1,25 @@
// Code generated by "genfeature -receiver *Client"; DO NOT EDIT.

package muc

import (
	"mellium.im/xmpp/disco/info"
)

// A list of service discovery features that are supported by this package.
var (
	Feature = info.Feature{Var: NS}
)

// ForFeatures implements info.FeatureIter.
func (*Client) ForFeatures(node string, f func(info.Feature) error) error {
	if node != "" {
		return nil
	}
	var err error
	err = f(Feature)
	if err != nil {
		return err
	}
	return nil
}