~emersion/soju-dev

Add support for a namespace extension v1 PROPOSED

Hubert Hirtz: 1
 Add support for a namespace extension

 4 files changed, 213 insertions(+), 10 deletions(-)
Export patchset (mbox)
How do I use this?

Copy & paste the following snippet into your terminal to import this patchset into git:

curl -s https://lists.sr.ht/~emersion/soju-dev/patches/25600/mbox | git am -3
Learn more about email & git
View this thread in the archives

[RFC PATCH] Add support for a namespace extension Export this patch

---

See the spec below for details, tldr: gamja makes too many websockets
for some corporate proxies, and this ext bypass the restriction by
merging all IRC connections... a bit of a hacky workaround until soju
supports QUIC! (lol.. unless?)

Repurposed the old spec [0] into a simpler one so that it's easier to
integrate in both parties.

Tested locally, it works, so I'm posting this patch here in case someone
encounters the same issues.

[0] https://lists.sr.ht/~emersion/soju-dev/patches/15392

 conn.go              |  52 +++++++++++++++++-----
 doc/ext/namespace.md | 101 +++++++++++++++++++++++++++++++++++++++++++
 downstream.go        |  13 ++++++
 namespaces.go        |  57 ++++++++++++++++++++++++
 4 files changed, 213 insertions(+), 10 deletions(-)
 create mode 100644 doc/ext/namespace.md
 create mode 100644 namespaces.go

diff --git a/conn.go b/conn.go
index e25f7da..5aa1e69 100644
--- a/conn.go
+++ b/conn.go
@@ -172,6 +172,8 @@ type conn struct {
	lock     sync.Mutex
	outgoing chan<- *irc.Message
	closed   bool

	children map[string]chan *irc.Message
}

func newConn(srv *Server, ic ircConn, options *connOptions) *conn {
@@ -181,6 +183,7 @@ func newConn(srv *Server, ic ircConn, options *connOptions) *conn {
		srv:      srv,
		outgoing: outgoing,
		logger:   options.Logger,
		children: make(map[string]chan *irc.Message),
	}

	go func() {
@@ -241,18 +244,29 @@ func (c *conn) Close() error {
}

func (c *conn) ReadMessage() (*irc.Message, error) {
	msg, err := c.conn.ReadMessage()
	if isErrClosed(err) {
		return nil, io.EOF
	} else if err != nil {
		return nil, err
	}
	for {
		msg, err := c.conn.ReadMessage()
		if isErrClosed(err) {
			return nil, io.EOF
		} else if err != nil {
			return nil, err
		}

	if c.srv.Debug {
		c.logger.Printf("received: %v", msg)
	}
		namespace, hasNamespace := msg.GetTag("soju.im/namespace")
		delete(msg.Tags, "soju.im/namespace")
		if hasNamespace {
			if child, ok := c.children[namespace]; ok {
				child <- msg
				continue
			}
		}

	return msg, nil
		if c.srv.Debug {
			c.logger.Printf("received: %v", msg)
		}

		return msg, nil
	}
}

// SendMessage queues a new outgoing message. It is safe to call from any
@@ -277,3 +291,21 @@ func (c *conn) RemoteAddr() net.Addr {
func (c *conn) LocalAddr() net.Addr {
	return c.conn.LocalAddr()
}

func (c *conn) NamespacedConn(namespace string) *namespacedConn {
	if msgs, ok := c.children[namespace]; ok {
		return &namespacedConn{
			inner:     c.conn,
			in:        msgs,
			namespace: namespace,
		}
	}
	msgs := make(chan *irc.Message, 16)
	c.children[namespace] = msgs
	nc := &namespacedConn{
		inner:     c.conn,
		in:        msgs,
		namespace: namespace,
	}
	return nc
}
diff --git a/doc/ext/namespace.md b/doc/ext/namespace.md
new file mode 100644
index 0000000..f35d8bf
--- /dev/null
+++ b/doc/ext/namespace.md
@@ -0,0 +1,101 @@
---
title: Namespace extension
layout: spec
work-in-progress: true
copyrights:
  -
    name: "Hubert Hirtz"
    period: "2021"
    email: "hubert@hirtz.pm"
---

## Notes for implementing experimental vendor extension

This is an experimental specification for a vendored extension.

No guarantees are made regarding the stability of this extension.
Backwards-incompatible changes can be made at any time without prior notice.

Software implementing this work-in-progress specification MUST NOT use the
unprefixed `namespace` CAP names. Instead, implementations SHOULD use
the `soju.im/namespace` CAP names to be interoperable with other software
implementing a compatible work-in-progress version.

## Description

This document describes the `namespace` extension. This enables connection
multiplexing, which is useful in the context of bouncer-networks with corporate
proxies (oh no simons...).

Multiple IRC connections can thus live on the same underlying IRC connection.

## Implementation

This extension introduces the `namespace` capability, the `namespace` message
tag and the `NAMESPACE` command.

The `NAMESPACE` command has one parameter, which must follow the same syntax as
batch reference tags.

The `namespace` tag as a mandatory, opaque, associated value.

### Connection life-cycle

This section assumes the `namespace` capability has been successfully
negotiated.

The underlying IRC connection, which has been used to negotiate the capabilities
required by this extension, is called the **parent connection**.  The IRC
connections initiated from the use of this extension are called **child
connections**.

Clients MAY request a new child connection be opened with the `NAMESPACE`
command.  The parameter sent with this command is the identifier of this child
connection.  Once the server replies back with the same command and the same
parameter, the child connection is opened.

Messages that feature a `namespace` tag belong to the opened child connection
whose identifier is the tag value.  Messages that do not feature such a tag
belong to the parent connection.

A child connection is closed when the server sends an `ERROR` command with a
`namespace` tag whose value is the identifier of this child connection.

Child connections neither share state with each other, nor with the parent
connection, meaning capability negotiation and registration must be done
independently on each opened child connection.

Child connections MAY NOT be parent connections.

Child connections are implicitely closed when the parent connection closes.

## Examples

```
Client: CAP REQ :soju.im/namespace
Client: NICK parent
Client: USER parent 0 * :Parent connection
Client: CAP END
Server: :example.com CAP ACK :soju.im/namespace
Server: :example.com 001 nick :Welcome home, parent
Server: :example.com ...
Server: :example.com 376 nick :The end of the MOTD/welcome burst
Client: NAMESPACE dan
Server: NAMESPACE dan
Client: @namespace=dan NICK child
Client: @namespace=dan USER child 0 * :Child connection
Server: @namespace=dan :example.com 001 nick :Welcome home, child
Server: @namespace=dan :example.com ...
Server: @namespace=dan :example.com 376 nick :The end of the MOTD/welcome burst
Client: @namespace=dan QUIT
Server: @namespace=dan ERROR :closed namespace
Client: NAMESPACE progval
Server: NAMESPACE progval
Client: @namespace=progval NICK child2
Client: @namespace=progval USER child2 0 * :Child connection #2
Server: @namespace=progval :example.com 001 nick :Welcome home, child2
Server: @namespace=progval :example.com ...
Server: @namespace=progval :example.com 376 nick :The end of the MOTD/welcome burst
Client: QUIT
Server: ERROR :closed link
```
diff --git a/downstream.go b/downstream.go
index 697b661..09f9c27 100644
--- a/downstream.go
+++ b/downstream.go
@@ -126,6 +126,7 @@ var permanentDownstreamCaps = map[string]string{

	"soju.im/bouncer-networks":        "",
	"soju.im/bouncer-networks-notify": "",
	"soju.im/namespace":               "",
}

// needAllDownstreamCaps is the list of downstream capabilities that
@@ -2385,6 +2386,18 @@ func (dc *downstreamConn) handleMessageRegistered(msg *irc.Message) error {
				Params:  []string{"BOUNCER", "UNKNOWN_COMMAND", subcommand, "Unknown subcommand"},
			}}
		}
	case "NAMESPACE":
		var namespace string
		if err := parseMessageParams(msg, &namespace); err != nil {
			return err
		}

		child := dc.NamespacedConn(namespace)
		dc.SendMessage(&irc.Message{
			Command: "NAMESPACE",
			Params:  []string{namespace},
		})
		go dc.srv.handle(child)
	default:
		dc.logger.Printf("unhandled message: %v", msg)

diff --git a/namespaces.go b/namespaces.go
new file mode 100644
index 0000000..e55710b
--- /dev/null
+++ b/namespaces.go
@@ -0,0 +1,57 @@
package soju

import (
	"io"
	"net"
	"time"

	"gopkg.in/irc.v3"
)

type namespacedConn struct {
	inner     ircConn
	in        <-chan *irc.Message
	namespace string
}

func (nc *namespacedConn) ReadMessage() (*irc.Message, error) {
	msg, ok := <-nc.in
	if !ok || msg == nil {
		return nil, io.EOF
	}
	return msg, nil
}

func (nc *namespacedConn) WriteMessage(msg *irc.Message) error {
	if msg.Tags == nil {
		msg.Tags = make(map[string]irc.TagValue)
	}
	msg.Tags["soju.im/namespace"] = irc.TagValue(nc.namespace)
	return nc.inner.WriteMessage(msg)
}

func (nc *namespacedConn) Close() error {
	return nc.inner.WriteMessage(&irc.Message{
		Tags: map[string]irc.TagValue{
			"namespace": irc.TagValue(nc.namespace),
		},
		Command: "ERROR",
		Params:  []string{"closed namespace"},
	})
}

func (nc *namespacedConn) SetReadDeadline(t time.Time) error {
	return nc.inner.SetReadDeadline(t)
}

func (nc *namespacedConn) SetWriteDeadline(t time.Time) error {
	return nc.inner.SetWriteDeadline(t)
}

func (nc *namespacedConn) RemoteAddr() net.Addr {
	return nc.inner.RemoteAddr()
}

func (nc *namespacedConn) LocalAddr() net.Addr {
	return nc.inner.LocalAddr()
}
-- 
2.33.0