~samwhited/xmpp

ab527bb8d3f1af940f5064e744c159565a4c950c — Sam Whited a month ago f051907
internal/integration: add s2s connection support

Signed-off-by: Sam Whited <sam@samwhited.com>
M internal/integration/ejabberd/config.go => internal/integration/ejabberd/config.go +10 -0
@@ 13,6 13,7 @@ import (
type Config struct {
	VHosts    []string
	C2SSocket string
	S2SSocket string
}

const inetrc = `{lookup,["file","native"]}.


@@ 35,6 36,7 @@ certfiles:
loglevel: info

listen:
{{- if .C2SSocket }}
  -
    port: "unix:{{ .C2SSocket }}"
    ip: "::1"


@@ 43,6 45,14 @@ listen:
    shaper: c2s_shaper
    access: c2s
    starttls_required: true
{{- end }}
{{- if .S2SSocket }}
  -
    port: "unix:{{ .S2SSocket }}"
    ip: "::1"
    module: ejabberd_s2s_in
    max_stanza_size: 524288
{{- end }}

s2s_use_starttls: optional


M internal/integration/ejabberd/ejabberd.go => internal/integration/ejabberd/ejabberd.go +7 -0
@@ 80,10 80,17 @@ func defaultConfig(cmd *integration.Cmd) error {
	}
	c2sSocket := c2sListener.Addr().(*net.UnixAddr).Name

	s2sListener, err := cmd.S2SListen("unix", filepath.Join(cmd.ConfigDir(), "s2s.socket"))
	if err != nil {
		return err
	}
	s2sSocket := s2sListener.Addr().(*net.UnixAddr).Name

	// The config file didn't exist, so create a default config.
	return ConfigFile(Config{
		VHosts:    []string{"localhost"},
		C2SSocket: c2sSocket,
		S2SSocket: s2sSocket,
	})(cmd)
}


M internal/integration/integration.go => internal/integration/integration.go +60 -10
@@ 15,6 15,7 @@ import (
	"crypto/rsa"
	"crypto/x509"
	"encoding/pem"
	"errors"
	"fmt"
	"io"
	"io/ioutil"


@@ 45,7 46,9 @@ type Cmd struct {
	deferF      []func(*Cmd) error
	in, out     *testWriter
	c2sListener net.Listener
	s2sListener net.Listener
	c2sNetwork  string
	s2sNetwork  string
	shutdown    func(*Cmd) error
}



@@ 91,6 94,20 @@ func (cmd *Cmd) C2SListen(network, addr string) (net.Listener, error) {
	return cmd.c2sListener, err
}

// S2SListen returns a listener with a random port.
// The listener is created on the first call to S2SListener.
// Subsequent calls ignore the arguments and return the existing listener.
func (cmd *Cmd) S2SListen(network, addr string) (net.Listener, error) {
	if cmd.s2sListener != nil {
		return cmd.s2sListener, nil
	}

	var err error
	cmd.s2sListener, err = net.Listen(network, addr)
	cmd.s2sNetwork = network
	return cmd.s2sListener, err
}

// ConfigDir returns the temporary directory used to store config files.
func (cmd *Cmd) ConfigDir() string {
	return cmd.cfgDir


@@ 114,12 131,37 @@ func (cmd *Cmd) Close() error {
	return e
}

// Dial attempts to connect to the server by dialing localhost and then
// negotiating a stream with the location set to the domainpart of j and the
// origin set to j.
func (cmd *Cmd) Dial(ctx context.Context, j jid.JID, t *testing.T, features ...xmpp.StreamFeature) (*xmpp.Session, error) {
// DialClient attempts to connect to the server with a client-to-server (c2s)
// connection by dialing the address reserved by C2SListen and then negotiating
// a stream with the location set to the domainpart of j and the origin set to
// j.
func (cmd *Cmd) DialClient(ctx context.Context, j jid.JID, t *testing.T, features ...xmpp.StreamFeature) (*xmpp.Session, error) {
	return cmd.dial(ctx, false, j.Domain(), j, t, features...)
}

// DialServer attempts to connect to the server with a server-to-server (s2s)
// connection by dialing the address reserved by S2SListen and then negotiating
// a stream.
func (cmd *Cmd) DialServer(ctx context.Context, location, origin jid.JID, t *testing.T, features ...xmpp.StreamFeature) (*xmpp.Session, error) {
	return cmd.dial(ctx, true, location, origin, t, features...)
}

func (cmd *Cmd) dial(ctx context.Context, s2s bool, location, origin jid.JID, t *testing.T, features ...xmpp.StreamFeature) (*xmpp.Session, error) {
	switch {
	case s2s && cmd.s2sListener == nil:
		return nil, errors.New("s2s not configured, please call S2SListen first")
	case !s2s && cmd.c2sListener == nil:
		return nil, errors.New("c2s not configured, please call C2SListen first")
	}

	addr := cmd.c2sListener.Addr().String()
	conn, err := net.Dial(cmd.c2sNetwork, addr)
	network := cmd.c2sNetwork
	if s2s {
		addr = cmd.s2sListener.Addr().String()
		network = cmd.s2sNetwork
	}

	conn, err := net.Dial(network, addr)
	if err != nil {
		return nil, fmt.Errorf("error dialing %s: %w", addr, err)
	}


@@ 130,8 172,8 @@ func (cmd *Cmd) Dial(ctx context.Context, j jid.JID, t *testing.T, features ...x
	})
	session, err := xmpp.NegotiateSession(
		ctx,
		j.Domain(),
		j,
		location,
		origin,
		conn,
		false,
		negotiator,


@@ 322,9 364,17 @@ func Test(ctx context.Context, name string, t *testing.T, opts ...Option) Subtes
	if err != nil {
		t.Fatal(err)
	}
	err = waitSocket(cmd.c2sNetwork, cmd.c2sListener.Addr().String())
	if err != nil {
		t.Fatal(err)
	if cmd.c2sListener != nil {
		err = waitSocket(cmd.c2sNetwork, cmd.c2sListener.Addr().String())
		if err != nil {
			t.Fatal(err)
		}
	}
	if cmd.s2sListener != nil {
		err = waitSocket(cmd.s2sNetwork, cmd.s2sListener.Addr().String())
		if err != nil {
			t.Fatal(err)
		}
	}
	for _, f := range cmd.deferF {
		err := f(cmd)

M internal/integration/prosody/config.go => internal/integration/prosody/config.go +11 -6
@@ 16,6 16,7 @@ type Config struct {
	Admins  []string
	VHosts  []string
	C2SPort int
	S2SPort int
}

const cfgBase = `daemonize = false


@@ 23,7 24,8 @@ pidfile = "{{ filepathJoin .ConfigDir "prosody.pid" }}"
admins = { {{ joinQuote .Admins }} }
data_path = "{{ .ConfigDir }}"
interfaces = { "::1" }
c2s_ports = { {{ .C2SPort }} }
{{ if .C2SPort }}c2s_ports = { {{ .C2SPort }} }{{ end }}
{{ if .S2SPort }}s2s_ports = { {{ .S2SPort }} }{{ end }}

modules_enabled = {



@@ 53,18 55,21 @@ modules_enabled = {
		"admin_adhoc"; -- Allows administration via an XMPP client that supports ad-hoc commands
}

modules_disabled = { "s2s" }
modules_disabled = {
  {{ if not .C2SPort }}"c2s";{{ end }}
  {{ if not .S2SPort }}"s2s";{{ end }}
}

allow_registration = false
c2s_require_encryption = true
-- s2s_require_encryption = true
-- s2s_secure_auth = false
-- s2s_insecure_domains = { {{ joinQuote .VHosts }} }
s2s_require_encryption = true
s2s_secure_auth = false
s2s_insecure_domains = { {{ joinQuote .VHosts }} }
authentication = "internal_plain"
storage = "internal"

log = {
  { levels = { min = "info" }, to = "console" };
	{ levels = { min = "info" }, to = "console" };
	{ levels = { min = "debug" }, to = "file", filename = "{{ filepathJoin .ConfigDir "prosody.log" }}" };
}


M internal/integration/prosody/prosody.go => internal/integration/prosody/prosody.go +13 -0
@@ 92,12 92,25 @@ func defaultConfig(cmd *integration.Cmd) error {
	if err != nil {
		return err
	}
	// Prosody creates its own sockets and doesn't provide us with a way of
	// pointing it at an existing Unix domain socket or handing the filehandle for
	// the TCP connection to it on start, so we're effectively just listening to
	// get a random port that we'll use to configure Prosody, then we need to
	// close the connection and let Prosody listen on that port.
	// Technically this is racey, but it's not likely to be a problem in practice.
	defer c2sListener.Close()

	s2sListener, err := cmd.S2SListen("tcp", "[::1]:0")
	if err != nil {
		return err
	}
	defer s2sListener.Close()

	// The config file didn't exist, so create a default config.
	return ConfigFile(Config{
		VHosts:  []string{"localhost"},
		C2SPort: c2sListener.Addr().(*net.TCPAddr).Port,
		S2SPort: s2sListener.Addr().(*net.TCPAddr).Port,
	})(cmd)
}


M ping/integration_test.go => ping/integration_test.go +1 -1
@@ 28,7 28,7 @@ func TestIntegrationSendPing(t *testing.T) {
		prosody.CreateUser(context.TODO(), j.String(), pass),
	)
	run(func(ctx context.Context, t *testing.T, cmd *integration.Cmd) {
		session, err := cmd.Dial(ctx, j, t,
		session, err := cmd.DialClient(ctx, j, t,
			xmpp.StartTLS(&tls.Config{
				InsecureSkipVerify: true,
			}),