~samwhited/xmpp

118369742fb100287e30df8fb546823290cf3dee — Sam Whited 5 years ago 8507f4e
Make JID API higher level (no PRECIS mentions)

Use the new golang.org/x/text/unicode/precis under the hood
4 files changed, 54 insertions(+), 161 deletions(-)

D jid/enforcedjid.go
M jid/jid.go
R jid/{preparedjid.go => safejid.go}
R jid/{preparedjid_test.go => safejid_test.go}
D jid/enforcedjid.go => jid/enforcedjid.go +0 -126
@@ 1,126 0,0 @@
// Copyright 2014 Sam Whited.
// Use of this source code is governed by the BSD 2-clause license that can be
// found in the LICENSE file.

package jid

import (
	"encoding/xml"
	"errors"
	"unicode/utf8"

	"golang.org/x/net/idna"
)

// EnforcedJID represents an XMPP address (Jabber ID) comprising a localpart,
// domainpart, and resourcepart, that has undergone the PRECIS preparation and
// enforcement steps. This is normally what servers will want to use for all
// internal JID's.
type EnforcedJID struct {
	localpart    string
	domainpart   string
	resourcepart string
}

// EnforcedFromString constructs a new EnforcedJID from the given string
// representation.
func EnforcedFromString(s string) (*EnforcedJID, error) {
	localpart, domainpart, resourcepart, err := SplitString(s)
	if err != nil {
		return nil, err
	}
	return EnforcedFromParts(localpart, domainpart, resourcepart)
}

// EnforcedFromParts constructs a new EnforcedJID from the given localpart,
// domainpart, and resourcepart.
func EnforcedFromParts(localpart, domainpart, resourcepart string) (*EnforcedJID, error) {
	// Ensure that parts are valid UTF-8 (and short circuit the rest of the
	// process if they're not). We'll check the domainpart after performing
	// the IDNA ToUnicode operation.
	if !utf8.ValidString(localpart) || !utf8.ValidString(resourcepart) {
		return nil, errors.New("JID contains invalid UTF-8")
	}

	domainpart, err := idna.ToUnicode(domainpart)
	if err != nil {
		return nil, err
	}

	if !utf8.ValidString(domainpart) {
		return nil, errors.New("Domainpart contains invalid UTF-8")
	}

	// TODO: Do enforcement properly

	if err := commonChecks(localpart, domainpart, resourcepart); err != nil {
		return nil, err
	}

	return &EnforcedJID{
		localpart:    localpart,
		domainpart:   domainpart,
		resourcepart: resourcepart,
	}, nil
}

// Bare returns a copy of the Jid without a resourcepart. This is sometimes
// called a "bare" JID.
func (j *EnforcedJID) Bare() *EnforcedJID {
	return &EnforcedJID{
		localpart:    j.localpart,
		domainpart:   j.domainpart,
		resourcepart: "",
	}
}

// Localpart gets the localpart of a JID (eg "username").
func (j *EnforcedJID) Localpart() string {
	return j.localpart
}

// Domainpart gets the domainpart of a JID (eg. "example.net").
func (j *EnforcedJID) Domainpart() string {
	return j.domainpart
}

// Resourcepart gets the resourcepart of a JID (eg. "someclient-abc123").
func (j *EnforcedJID) Resourcepart() string {
	return j.resourcepart
}

// Makes a copy of the given Jid. j.Equals(j.Copy()) will always return true.
func (j *EnforcedJID) Copy() *EnforcedJID {
	return &EnforcedJID{
		localpart:    j.localpart,
		domainpart:   j.domainpart,
		resourcepart: j.resourcepart,
	}
}

// String converts an EnforcedJID to its string representation.
func (j *EnforcedJID) String() string {
	return stringify(j)
}

// Equal performs an octet-for-octet comparison with the given JID.
func (j *EnforcedJID) Equal(j2 JID) bool {
	return j.Localpart() == j2.Localpart() &&
		j.Domainpart() == j2.Domainpart() && j.Resourcepart() == j2.Resourcepart()
}

// MarshalXMLAttr satisfies the MarshalerAttr interface and marshals the JID as
// an XML attribute.
func (j *EnforcedJID) MarshalXMLAttr(name xml.Name) (xml.Attr, error) {
	return xml.Attr{Name: name, Value: j.String()}, nil
}

// UnmarshalXMLAttr satisfies the UnmarshalerAttr interface and unmarshals an
// XML attribute into a valid JID (or returns an error).
func (j *EnforcedJID) UnmarshalXMLAttr(attr xml.Attr) error {
	jid, err := EnforcedFromString(attr.Value)
	j.localpart = jid.Localpart()
	j.domainpart = jid.Domainpart()
	j.resourcepart = jid.Resourcepart()
	return err
}

M jid/jid.go => jid/jid.go +1 -1
@@ 127,7 127,7 @@ func commonChecks(localpart, domainpart, resourcepart string) error {

	// RFC 7622 §3.3.1 provides a small table of characters which are still not
	// allowed in localpart's even though the IdentifierClass base class and the
	// UsernameCaseMapped profile don't forbid them; remove them here.
	// UsernameCaseMapped profile don't forbid them; disallow them here.
	if strings.ContainsAny(localpart, "\"&'/:<>@") {
		return errors.New("Localpart contains forbidden characters")
	}

R jid/preparedjid.go => jid/safejid.go +47 -28
@@ 10,30 10,33 @@ import (
	"unicode/utf8"

	"golang.org/x/net/idna"
	"golang.org/x/text/unicode/precis"
)

// PreparedJID represents an XMPP address (Jabber ID) comprising a localpart,
// domainpart, and resourcepart, that has undergone the PRECIS preparation step.
// This is normally what clients will want to use for all internal JID's.
type PreparedJID struct {
// SafeJID represents an XMPP address (Jabber ID) comprising a localpart,
// domainpart, and resourcepart, that is safe to display in a user interface,
// send over the wire, or compare with another SafeJID. All parts of a safe JID
// are guaranteed to be valid UTF-8 and will be represented in their canonical
// form which gives comparison the greatest chance of succeeding.
type SafeJID struct {
	localpart    string
	domainpart   string
	resourcepart string
}

// PreparedFromString constructs a new PreparedJID from the given string
// SafeFromString constructs a new SafeJID from the given string
// representation.
func PreparedFromString(s string) (*PreparedJID, error) {
func SafeFromString(s string) (*SafeJID, error) {
	localpart, domainpart, resourcepart, err := SplitString(s)
	if err != nil {
		return nil, err
	}
	return PreparedFromParts(localpart, domainpart, resourcepart)
	return SafeFromParts(localpart, domainpart, resourcepart)
}

// PreparedFromParts constructs a new PreparedJID from the given localpart,
// SafeFromParts constructs a new SafeJID from the given localpart,
// domainpart, and resourcepart.
func PreparedFromParts(localpart, domainpart, resourcepart string) (*PreparedJID, error) {
func SafeFromParts(localpart, domainpart, resourcepart string) (*SafeJID, error) {
	// Ensure that parts are valid UTF-8 (and short circuit the rest of the
	// process if they're not). We'll check the domainpart after performing
	// the IDNA ToUnicode operation.


@@ 49,11 52,9 @@ func PreparedFromParts(localpart, domainpart, resourcepart string) (*PreparedJID
	//    [RFC5890].  This implies that the string MUST NOT include A-labels as
	//    defined in [RFC5890]; each A-label MUST be converted to a U-label
	//    during preparation of a string for inclusion in a domainpart slot.
	//
	// While we're not doing preparation yet, we're also going to store all JIDs
	// as Unicode strings, so let's go ahead and do this (even for Unsafe JID's).

	domainpart, err := idna.ToUnicode(domainpart)
	var err error
	domainpart, err = idna.ToUnicode(domainpart)
	if err != nil {
		return nil, err
	}


@@ 62,13 63,31 @@ func PreparedFromParts(localpart, domainpart, resourcepart string) (*PreparedJID
		return nil, errors.New("Domainpart contains invalid UTF-8")
	}

	// TODO: Do preparation properly
	// RFC 7622 §3.2.2.  Enforcement
	//
	//   An entity that performs enforcement in XMPP domainpart slots MUST
	//   prepare a string as described in Section 3.2.1 and MUST also apply
	//   the normalization, case-mapping, and width-mapping rules defined in
	//   [RFC5892].
	//
	// TODO: I have no idea what this is talking about… what rules? RFC 5892 is a
	//       bunch of property lists. Maybe it meant RFC 5895?

	localpart, err = precis.UsernameCaseMapped.String(localpart)
	if err != nil {
		return nil, err
	}

	resourcepart, err = precis.OpaqueString.String(resourcepart)
	if err != nil {
		return nil, err
	}

	if err := commonChecks(localpart, domainpart, resourcepart); err != nil {
		return nil, err
	}

	return &PreparedJID{
	return &SafeJID{
		localpart:    localpart,
		domainpart:   domainpart,
		resourcepart: resourcepart,


@@ 77,8 96,8 @@ func PreparedFromParts(localpart, domainpart, resourcepart string) (*PreparedJID

// Bare returns a copy of the Jid without a resourcepart. This is sometimes
// called a "bare" JID.
func (j *PreparedJID) Bare() *PreparedJID {
	return &PreparedJID{
func (j *SafeJID) Bare() *SafeJID {
	return &SafeJID{
		localpart:    j.localpart,
		domainpart:   j.domainpart,
		resourcepart: "",


@@ 86,50 105,50 @@ func (j *PreparedJID) Bare() *PreparedJID {
}

// Localpart gets the localpart of a JID (eg "username").
func (j *PreparedJID) Localpart() string {
func (j *SafeJID) Localpart() string {
	return j.localpart
}

// Domainpart gets the domainpart of a JID (eg. "example.net").
func (j *PreparedJID) Domainpart() string {
func (j *SafeJID) Domainpart() string {
	return j.domainpart
}

// Resourcepart gets the resourcepart of a JID (eg. "someclient-abc123").
func (j *PreparedJID) Resourcepart() string {
func (j *SafeJID) Resourcepart() string {
	return j.resourcepart
}

// Makes a copy of the given Jid. j.Equals(j.Copy()) will always return true.
func (j *PreparedJID) Copy() *PreparedJID {
	return &PreparedJID{
func (j *SafeJID) Copy() *SafeJID {
	return &SafeJID{
		localpart:    j.localpart,
		domainpart:   j.domainpart,
		resourcepart: j.resourcepart,
	}
}

// String converts an PreparedJID to its string representation.
func (j *PreparedJID) String() string {
// String converts an SafeJID to its string representation.
func (j *SafeJID) String() string {
	return stringify(j)
}

// Equal performs an octet-for-octet comparison with the given JID.
func (j *PreparedJID) Equal(j2 JID) bool {
func (j *SafeJID) Equal(j2 JID) bool {
	return j.Localpart() == j2.Localpart() &&
		j.Domainpart() == j2.Domainpart() && j.Resourcepart() == j2.Resourcepart()
}

// MarshalXMLAttr satisfies the MarshalerAttr interface and marshals the JID as
// an XML attribute.
func (j *PreparedJID) MarshalXMLAttr(name xml.Name) (xml.Attr, error) {
func (j *SafeJID) MarshalXMLAttr(name xml.Name) (xml.Attr, error) {
	return xml.Attr{Name: name, Value: j.String()}, nil
}

// UnmarshalXMLAttr satisfies the UnmarshalerAttr interface and unmarshals an
// XML attribute into a valid JID (or returns an error).
func (j *PreparedJID) UnmarshalXMLAttr(attr xml.Attr) error {
	jid, err := PreparedFromString(attr.Value)
func (j *SafeJID) UnmarshalXMLAttr(attr xml.Attr) error {
	jid, err := SafeFromString(attr.Value)
	j.localpart = jid.Localpart()
	j.domainpart = jid.Domainpart()
	j.resourcepart = jid.Resourcepart()

R jid/preparedjid_test.go => jid/safejid_test.go +6 -6
@@ 7,28 7,28 @@ import (
	"testing"
)

// PreparedJID's cannot contain invalid UTF8 in the localpart.
// SafeJID's cannot contain invalid UTF8 in the localpart.
func TestNewInvalidUtf8Localpart(t *testing.T) {
	invalid := string([]byte{0xff, 0xfe, 0xfd})
	_, err := PreparedFromString(invalid + "@example.com/resourcepart")
	_, err := SafeFromString(invalid + "@example.com/resourcepart")
	if err == nil {
		t.FailNow()
	}
}

// PreparedJID's cannot contain invalid UTF8 in the domainpart.
// SafeJID's cannot contain invalid UTF8 in the domainpart.
func TestNewInvalidUtf8Domainpart(t *testing.T) {
	invalid := string([]byte{0xff, 0xfe, 0xfd})
	_, err := PreparedFromString("example@" + invalid + "/resourcepart")
	_, err := SafeFromString("example@" + invalid + "/resourcepart")
	if err == nil {
		t.FailNow()
	}
}

// PreparedJID's cannot contain invalid UTF8 in the resourcepart.
// SafeJID's cannot contain invalid UTF8 in the resourcepart.
func TestNewInvalidUtf8Resourcepart(t *testing.T) {
	invalid := string([]byte{0xff, 0xfe, 0xfd})
	_, err := PreparedFromString("example@example.com/" + invalid)
	_, err := SafeFromString("example@example.com/" + invalid)
	if err == nil {
		t.FailNow()
	}